rusty-proxy - Web
Points: 439 | Flag: BITSCTF{tr4il3r_p4r51n6_15_p41n_1n_7h3_4hh} | Solved by: Smothy @ 0xN1umb

what we got
zip file with source for a Rust reverse proxy + Flask backend. docker-compose setup, pretty standard stuff.
backend (server.py) has three routes:
/- index/api/status- status page/admin/flag- the flag lol
the rust proxy sits in front and forwards requests to the backend. but it blocks /admin:
fn is_path_allowed(path: &str) -> bool {
let normalized = path.to_lowercase();
if normalized.starts_with("/admin") {
return false;
}
true
}so to_lowercase() means we cant just do /Admin or /ADMIN. case tricks are out. ok cool what else we got
the solve
ngl this was a one-shot solve lmao
the proxy checks the raw path string but never URL-decodes it. Flask on the backend? yeah it decodes URLs automatically because thats what WSGI servers do.
so the bypass is literally just URL-encoding one character in "admin":
/%61dmin/flag
%61 = a in URL encoding. the proxy sees /%61dmin/flag, lowercases it to /%61dmin/flag (percent encoding stays the same obv), checks if it starts with /admin — nope! forwards it through.
Flask receives /%61dmin/flag, decodes it to /admin/flag, routes it to the flag endpoint. gg
$ curl http://rusty-proxy.chals.bitskrieg.in:25001/%61dmin/flag
{"flag":"BITSCTF{tr4il3r_p4r51n6_15_p41n_1n_7h3_4hh}"}
thats it. thats the whole challenge. classic proxy vs backend path parsing mismatch. the flag even says it — "trailer parsing is pain in the ahh" fr fr
lowkey the rust code was actually pretty solid for everything else (header validation, chunked encoding handling, connection pooling) but they forgot the one thing that matters — normalize your paths before you check em
could also do stuff like /%2561dmin (double encoding), /./admin/flag, or even /%41dmin/flag (%41 = A, which Flask would decode then match case-insensitively). but the simplest %61 worked first try so why overcomplicate it
flag
BITSCTF{tr4il3r_p4r51n6_15_p41n_1n_7h3_4hh}
smothy out ✌️