Post

OverTheWire Natas: Levels 22–26

OverTheWire Natas: Levels 22–26

Level 22 — PHP Header Redirect Bypass

Information disclosureCWE-200

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 source redirecting non-admins before they see the body

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.

Requesting with revelio set and redirects disabled

The un-followed response contained the credentials for natas23.

The captured body with the natas23 credentials

Level 23 — PHP Type Juggling

Type jugglingCWE-697

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).

The two conditions on the password parameter

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.

Submitting the type-juggling payload

Sending passwd=11iloveyou returned the credentials for natas24.

The natas24 credentials

Level 24 — strcmp() Array Bypass

Type jugglingCWE-697

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 strcmp-based password check

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.

Sending passwd as an array

strcmp() received an array, returned NULL, and the check passed — printing a type-warning followed by the credentials for natas25.

The strcmp warning and the natas25 credentials

Level 25 — Traversal Filter Bypass and Log Poisoning

Local file inclusionCWE-98

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 non-recursive traversal filter and the logging function

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.

The form and the listFiles helper

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"]

Poisoning the log via User-Agent, then including it through the traversal bypass

The included log executed and surfaced the password for natas26.

The poisoned log executing and revealing the password

Level 26 — PHP Object Deserialization

Insecure deserializationCWE-502

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.

The Logger class with a file-writing __destruct method

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.

The code unserializing the drawing cookie

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.

The form and session handling for the drawing feature

When the server deserialized my cookie, the Logger’s __destruct() fired and wrote my PHP payload into img/natas26_pwned.php.

Crafting the malicious Logger object and dropping the webshell

Requesting that dropped file executed the payload and printed the password for natas27.

The dropped PHP file returning the natas27 password


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.

This post is licensed under CC BY-NC-ND 4.0 by the author.