Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/pentesting-web/deserialization/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,64 @@ $o->param = "PARAM";
$ser=serialize($o);
```

### Preventing PHP Object Injection with `allowed_classes`

> [!INFO]
> Support for the **second argument** of `unserialize()` (the `$options` array) was added in **PHP 7.0**. On older versions the function only accepts the serialized string, making it impossible to restrict which classes may be instantiated.

`unserialize()` will **instantiate every class** it finds inside the serialized stream unless told otherwise. Since PHP 7 the behaviour can be restricted with the [`allowed_classes`](https://www.php.net/manual/en/function.unserialize.php) option:

```php
// NEVER DO THIS – full object instantiation
$object = unserialize($userControlledData);

// SAFER – disable object instantiation completely
$object = unserialize($userControlledData, [
'allowed_classes' => false // no classes may be created
]);

// Granular – only allow a strict white-list of models
$object = unserialize($userControlledData, [
'allowed_classes' => [MyModel::class, DateTime::class]
]);
```

If **`allowed_classes` is omitted _or_ the code runs on PHP < 7.0**, the call becomes **dangerous** as an attacker can craft a payload that abuses magic methods such as `__wakeup()` or `__destruct()` to achieve Remote Code Execution (RCE).

#### Real-world example: Everest Forms (WordPress) CVE-2025-52709

The WordPress plugin **Everest Forms ≤ 3.2.2** tried to be defensive with a helper wrapper but forgot about legacy PHP versions:

```php
function evf_maybe_unserialize($data, $options = array()) {
if (is_serialized($data)) {
if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
// SAFE branch (PHP ≥ 7.1)
$options = wp_parse_args($options, array('allowed_classes' => false));
return @unserialize(trim($data), $options);
}
// DANGEROUS branch (PHP < 7.1)
return @unserialize(trim($data));
}
return $data;
}
```

On servers that still ran **PHP ≤ 7.0** this second branch led to a classic **PHP Object Injection** when an administrator opened a malicious form submission. A minimal exploit payload could look like:

```
O:8:"SomeClass":1:{s:8:"property";s:28:"<?php system($_GET['cmd']); ?>";}
```

As soon as the admin viewed the entry, the object was instantiated and `SomeClass::__destruct()` got executed, resulting in arbitrary code execution.

**Take-aways**
1. Always pass `['allowed_classes' => false]` (or a strict white-list) when calling `unserialize()`.
2. Audit defensive wrappers – they often forget about the legacy PHP branches.
3. Upgrading to **PHP ≥ 7.x** alone is *not* sufficient: the option still needs to be supplied explicitly.

---

### PHPGGC (ysoserial for PHP)

[**PHPGGC**](https://github.com/ambionics/phpggc) can help you generating payloads to abuse PHP deserializations.\
Expand Down Expand Up @@ -663,6 +721,8 @@ The tool [JMET](https://github.com/matthiaskaiser/jmet) was created to **connect

### References

- [Patchstack advisory – Everest Forms unauthenticated PHP Object Injection (CVE-2025-52709)](https://patchstack.com/articles/critical-vulnerability-impacting-over-100k-sites-patched-in-everest-forms-plugin/)

- JMET talk: [https://www.youtube.com/watch?v=0h8DWiOWGGA](https://www.youtube.com/watch?v=0h8DWiOWGGA)
- Slides: [https://www.blackhat.com/docs/us-16/materials/us-16-Kaiser-Pwning-Your-Java-Messaging-With-Deserialization-Vulnerabilities.pdf](https://www.blackhat.com/docs/us-16/materials/us-16-Kaiser-Pwning-Your-Java-Messaging-With-Deserialization-Vulnerabilities.pdf)

Expand Down