OverTheWire Natas: Levels 22–26
Level 22 — PHP Header Redirect Bypass
Level 22 looked empty in the browser. The source explained why: when the revelio parameter is present, the page checks whether you’re an admin and, if you’re not, calls header('Location: /') to redirect you away. Browsers follow that redirect instantly and never render the original response — but the credentials were printed in that response body all the same.
The key insight is that a Location header doesn’t erase the response body — it just tells the browser to navigate elsewhere. If I simply don’t follow the redirect, the body is right there. I requested the page with revelio=1 and set allow_redirects=False in Python’s requests so it would hand me the raw response instead of chasing the redirect.
The un-followed response contained the credentials for natas23.
Level 23 — PHP Type Juggling
Level 23 put two conditions on the passwd parameter: it had to contain the string iloveyou (checked with strstr()), and it had to be “greater than 10” (checked with a loose > comparison against an integer).
Those two requirements seem contradictory until you remember how PHP’s loose comparison coerces types. When a string is compared to an integer, PHP casts the string to a number first — and a string that starts with digits becomes the value of those leading digits. So 11iloveyou satisfies both checks at once: strstr() finds iloveyou, and the string cast to an integer yields 11, which is greater than 10.
Sending passwd=11iloveyou returned the credentials for natas24.
Level 24 — strcmp() Array Bypass
Level 24’s password check used strcmp(): if(!strcmp($_REQUEST['passwd'], $secretpassword)). Since strcmp() returns 0 for equal strings and !0 is true, the intent was that only the correct password passes.
The weakness is in how strcmp() handles bad input. If either argument is an array rather than a string, older PHP versions return NULL (with a warning) instead of an integer — and !NULL evaluates to true, passing the check. PHP turns any parameter whose name ends in [] into an array, so I just submitted passwd[]=anything.
strcmp() received an array, returned NULL, and the check passed — printing a type-warning followed by the credentials for natas25.
Level 25 — Traversal Filter Bypass and Log Poisoning
Level 25 included a language file based on a lang parameter. It tried to block path traversal by stripping ../ from the input with str_replace(), and it specifically aborted if the filename contained natas_webpass. Two things stood out: the strip wasn’t recursive, and a separate logRequest() function wrote the raw User-Agent header into a log file at a predictable path keyed by session ID.
The filter bypass relies on the strip running only once. The sequence ....// contains ../ in the middle, so a single pass of str_replace() removes that inner ../ and leaves behind a clean ../ — letting me traverse out of the language directory after all. The natas_webpass block meant I couldn’t read the password file directly, so I needed code execution instead.
That’s where the unsanitized logging comes in. On the first request I set my User-Agent to a PHP one-liner — <?php echo file_get_contents('/etc/natas_webpass/natas26'); ?> — which got written verbatim into my session’s log file. On the second request I used the ....// traversal to point lang at that same log file. PHP included the log, executed my injected code, and returned the password.
flowchart LR
A["Req 1: User-Agent =<br>PHP one-liner"] --> B["Written verbatim into<br>my session's log file"]
B --> C["Req 2: lang=....// traversal<br>includes that log file"]
C --> D["PHP executes the logged code<br>→ natas26 password"]
The included log executed and surfaced the password for natas26.
Level 26 — PHP Object Deserialization
Level 26 stored drawing data as a serialized, base64-encoded PHP object in a drawing cookie, and on each request it base64-decoded and ran unserialize() on that cookie directly. The source also defined a Logger class with a __destruct() magic method that writes its $exitMsg property to the file named in $logFile.
That combination is a classic insecure-deserialization gadget. PHP calls __destruct() automatically when a deserialized object goes out of scope, so if I can get a Logger object into the cookie, I control both what gets written and where. The drawing code happily unserializes whatever I hand it.
I crafted a serialized Logger whose $logFile pointed to a PHP file inside the web root (img/natas26_pwned.php) and whose $exitMsg held PHP code to read the natas27 password file. PHP’s serialization format prefixes private properties with the class name wrapped in null bytes (\x00Logger\x00logFile), so I built the string carefully, base64-encoded it, and sent it as the drawing cookie. A second request then fetched the file I’d just written.
When the server deserialized my cookie, the Logger’s __destruct() fired and wrote my PHP payload into img/natas26_pwned.php.
Requesting that dropped file executed the payload and printed the password for natas27.
The bugs are getting subtler — comparison quirks, an LFI hidden behind a log file, a deserialization gadget. The deepest cuts come next, in Natas: Levels 27–30.

















