OverTheWire Natas: Levels 31–34
This is the home stretch of Natas, and it ends the way the back half of the wargame has trended: less about spotting an obvious mistake and more about knowing the strange, load-bearing corners of a language’s runtime. Levels 31 and 32 are two faces of the same Perl footgun — the magic ARGV filehandle and the diamond operator — first for reading a file, then for running a command. Level 33 closes the series with a PHAR deserialization gadget, one of the more elegant exploitation primitives in PHP. Then natas34 hands you the end of the road.
Level 31 — Perl Diamond Operator File Read
Level 31 renders an uploaded CSV into an HTML table. Reading the Perl source, the line that stopped me was while (<$file>), where $file = $cgi->param('file'). That <...> is Perl’s diamond operator, and what it does with its operand is far weirder than it looks. If $file holds the string ARGV, then <$file> becomes <ARGV> — the special construct that reads from every filename sitting in Perl’s @ARGV array. And here’s the part that makes it exploitable: a CGI script populates @ARGV from the URL query string whenever that query string contains no = sign (a holdover from Perl’s ancient command-line-emulation behavior).
That gave me a clean two-part plan. I needed $cgi->param('file') to equal the string ARGV, and I needed $cgi->upload('file') to be truthy so the code enters the branch at all. The trick is to submit the file parameter twice: once as a plain field with the value ARGV (which param('file') returns), and once as an actual file upload (which makes upload('file') succeed). Then I put the file I actually wanted to read in the URL query string with no = in it, so it lands in @ARGV.
I pointed the query string at /etc/natas_webpass/natas32. The diamond read the password file line by line and the script dutifully rendered it as the table’s header row.
The principle I underlined: Perl’s <$fh> and ARGV are overloaded with decades of implicit behavior, and “read the uploaded file” and “read an arbitrary server file” are separated only by whether the developer pinned down what $file is allowed to be.
Level 32 — Diamond Operator to Command Execution
Level 32 is the same code as Level 31 — same diamond, same ARGV trick — but the password for natas33 isn’t sitting in a file I can read. It’s only obtainable by executing a helper binary, getpassword, that lives in the web directory. So the question became: can I push the ARGV technique one step further, from file disclosure into command execution?
The answer comes from the same overloaded open semantics that bit Level 29. When Perl opens a filename out of @ARGV and that name ends in a pipe (|), it doesn’t open a file at all — it runs the string as a command and reads the command’s output. So I reused the exact Level 31 harness — file sent as both ARGV and a real upload — but changed the query string to ./getpassword |. That trailing pipe converts the “filename” @ARGV entry into a command, and <ARGV> reads the binary’s stdout.
The server ran ./getpassword, and its output — the natas33 password — came back in the rendered table.
What made this one satisfying was recognizing that I didn’t need a new vulnerability — I needed a new use of the one I already understood. The gap between “read a file” and “run a command” was a single | character, because Perl’s open treats both as the same operation.
Level 33 — PHAR Deserialization
Level 33 is the finale, and it’s a proper gadget-chain puzzle. The source defines an Executor class whose __destruct() method does something dangerous: it chdirs into the upload directory, checks whether md5_file($this->filename) matches a stored $this->signature, and if so runs passthru("php " . $this->filename). In other words, if I can stand up an Executor object whose filename points at a PHP file I uploaded and whose signature is that file’s real MD5, the destructor will execute my code. The catch is that the normal upload path never lets me construct an Executor with attacker-controlled properties — those are set internally.
That’s exactly the situation PHAR deserialization is built for. PHP’s phar:// stream wrapper deserializes a PHAR archive’s metadata into live PHP objects whenever a filesystem function touches a phar:// path — and crucially, that deserialization fires the __destruct() of whatever object I embedded. So I don’t need the app to call new Executor(); I just need to smuggle a pre-built Executor object into a PHAR’s metadata and get the app to reference it via phar://.
I built the pieces locally. First a tiny webshell — <?php echo file_get_contents("/etc/natas_webpass/natas34"); ?> — and took its MD5, because that hash has to match the signature property for the destructor’s check to pass. Then I wrote a makephar.php that constructs test.phar, sets its metadata to an Executor object with filename set to shell.php and signature set to that MD5, and stamps a valid PHAR stub.
flowchart LR
A["Build test.phar:<br>metadata = Executor(shell.php, md5)"] --> B["Upload shell.php<br>(file exists on disk)"]
B --> C["Upload test.phar,<br>filename = phar://test.phar"]
C --> D["Filesystem op on phar:// →<br>metadata deserialized"]
D --> E["Executor __destruct():<br>md5 matches → passthru('php shell.php')"]
E --> F["Webshell reads natas34 password"]
Exploitation took two requests. First I uploaded shell.php itself so the file would physically exist in the server’s upload directory (that’s the file the destructor’s passthru("php ...") will run).
Then I uploaded test.phar, but set the filename field to phar://test.phar. When the app performed a filesystem operation on that phar:// path, PHP unpacked the archive’s metadata, reconstructed my Executor object, and — when it went out of scope — fired __destruct(). The MD5 check passed because I’d matched the signature to shell.php, and passthru("php shell.php") ran my code.
The webshell read the final password file, and the response confirmed it with Congratulations! Running firmware update: shell.php followed by the natas34 password.
The idea I want to remember from this one is that unserialize() doesn’t have to appear anywhere in the code for deserialization to be exploitable. Any filesystem function reachable with a phar:// path is an implicit deserialization sink, and a class with a side-effecting magic method (__destruct, __wakeup) is the gadget that turns it into code execution.
Level 34 — The End
natas34 has no challenge — it’s the victory page.
Looking back across all thirty-three levels, the thing that stuck with me wasn’t any single payload — it was how consistently the same root cause kept reappearing in new costumes: the server trusting something it shouldn’t. A hidden HTML comment, a forgeable cookie, a client-supplied filename, an “encrypted” query with no integrity check, an overloaded language API, a deserialization sink hiding behind a stream wrapper. Every level was a variation on the gap between what a developer assumed about their input and what the runtime would actually do with it. Training myself to live in that gap — to read code for its literal mechanics rather than its intent — is the real prize Natas hands you, and it’s the habit I’ll carry into everything that comes next.
That’s the whole Natas series. More web security writeups live under Web App Pentesting.











