Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Insecure Deserialization — PHAR, Custom Gadget Chain, SSTI via Twig
Difficulty Expert
Objective Delete morale.txt from Carlos's home directory
Note No explicit deserialization in the app — PHAR deserialization combined with a custom gadget chain and SSTI achieves RCE

Using PHAR Deserialization to Deploy a Custom Gadget Chain

I logged in as wiener:peter. There's an avatar upload feature that accepts only JPG images.

Screenshot

Uploaded avatars are served via /cgi-bin/avatar.php?avatar=wiener. Navigating to /cgi-bin/ showed a directory listing:

Screenshot
CustomTemplate.php~  Blog.php~  avatar.php

CustomTemplate.php~ contained a __destruct() that calls lockFilePath(), which concatenates $this->template_file_path as a string. If template_file_path is an object rather than a string, PHP coerces it by calling __toString() on it.

Blog.php~ was the more interesting one. Its __wakeup() loads $this->desc as a Twig template string, and __toString() renders it. Whatever ends up in $desc gets processed by Twig — making SSTI payloads in $desc execute during rendering.

The chain assembles as follows. PHAR deserialization fires __wakeup() on all serialized objects embedded in the PHAR metadata. Blog::__wakeup() loads $desc as a Twig template. CustomTemplate::__destruct() fires, calling lockFilePath(), which concatenates template_file_path as a string. Since template_file_path is a Blog object, PHP calls Blog::__toString() to coerce it. __toString() renders the Twig template — executing the SSTI payload. Three distinct vulnerability classes working together: PHAR deserialization as the delivery mechanism, the PHP magic method chain (__destruct__toString__wakeup), and Twig SSTI as the actual RCE sink. Any one alone isn't enough.

PHAR deserialization doesn't require an explicit unserialize() call anywhere in the application — any PHP filesystem function (file_exists(), file_get_contents(), unlink(), etc.) that receives a phar:// path triggers deserialization of the PHAR metadata, where the serialized objects live.

The Twig 1.x RCE payload: it registers exec as a callable for undefined filters, then calls it by requesting a non-existent filter whose name is the shell command:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}

The PHP payload:

<?php
class CustomTemplate {}
class Blog {}
$object = new CustomTemplate;
$blog = new Blog;
$blog->desc = '{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}';
$blog->user = 'user';
$object->template_file_path = $blog;
?>

I used phar-jpg-polyglot to embed this payload into a valid JPG file. The PHAR-JPG polyglot technique exploits the fact that a PHAR archive can be embedded inside a valid JPG — the upload filter checks the file signature and sees a JPG, while PHP treats it as a PHAR when given the phar:// stream wrapper.

Screenshot
Screenshot
Screenshot

The generated teto.jpg contains the PHAR archive embedded inside a valid JPG header. Uploading it as the avatar:

Screenshot

The upload succeeded — the server saw a valid JPG. Triggering PHAR deserialization by accessing the avatar with the phar:// stream wrapper:

GET /cgi-bin/avatar.php?avatar=phar://wiener
Screenshot

PHP processed the PHAR archive, deserializing the embedded objects. The gadget chain fired, Twig rendered the SSTI payload, and exec("rm /home/carlos/morale.txt") ran server-side.

This get's the lab solved

Resources