| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Insecure Deserialization — Arbitrary Object Injection, PHP Magic Method Abuse |
| Difficulty | Practitioner |
| Objective | Create and inject a malicious serialized object to delete morale.txt from Carlos's home directory |
Arbitrary Object Injection in PHP¶
I logged in as wiener:peter and decoded the session cookie from /my-account:
O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"af6dsjup0a1wbxqsxvuv76vqsyjadyxq";}
I tried the b:1 data-type bypass from the previous lab. The server returned a 500 with a PHP debug error confirming strict comparison is in use — that approach doesn't apply here. More useful was a comment in the page source:
<!-- TODO: Refactor once /libs/CustomTemplate.php is updated -->
Navigating to /libs/CustomTemplate.php returned nothing, but editor backup files are sometimes left behind with a trailing tilde:
web-security-academy.net/libs/CustomTemplate.php~
That returned the PHP source. The relevant part:
function __destruct() {
// Carlos thought this would be a good idea
if (file_exists($this->lock_file_path)) {
unlink($this->lock_file_path);
}
}
PHP's __destruct() magic method runs automatically when an object is garbage-collected — no explicit call needed. It calls unlink($this->lock_file_path), deleting whatever file lock_file_path points to. If a CustomTemplate object can be injected with lock_file_path set to /home/carlos/morale.txt, the destructor fires and deletes it the moment the deserialized object goes out of scope. That makes __destruct() a prime gadget starting point — it contains a file operation designed to clean up lock files, but nothing validates that the path is within expected bounds.
The class name in the payload must match an actual class in the application's code — unserialize() only instantiates classes it knows about. Constructing the malicious object using the real class name from the source:
O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}
Base64-encoding and setting it as the session cookie:
The server deserialized the object, instantiating CustomTemplate with lock_file_path = /home/carlos/morale.txt. When the object was destroyed, __destruct() fired and called unlink("/home/carlos/morale.txt").
Lab solved 8.8
Dead Ends & Rabbit Holes¶
Using a made-up class name (TetoTemplate) in the payload failed — unserialize() only instantiates classes that actually exist in the application. The class name must match CustomTemplate exactly as found in the source.