iCMS2's file-based cache backend serializes values with PHP's serialize() before writing them to disk, then reads them back with an unrestricted unserialize() call. If an attacker can write a crafted payload into the cache directory through any secondary vulnerability, the deserialization sink will instantiate attacker-controlled objects and trigger magic method gadget chains, which can lead to remote code execution with the web server's privileges.
Summary
| Field |
Value |
| Advisory |
GHSA-7mvq-hwhc-c98g |
| Severity |
High |
| CWE |
CWE-502 (Deserialization of Untrusted Data) |
| Package |
icms2 (PHP) |
| Affected |
< 2.18.1 |
| Vulnerability |
PHP Object Injection via unserialize() |
| Impact |
Remote Code Execution |
iCMS2's cmsCacheFiles class implements a file-based cache backend. It stores values by serializing them with serialize() and writing them into .php files on disk. When those files are read back, the stored value is passed directly to unserialize() with no restriction on which classes may be instantiated. If an attacker controls the contents of a cache file, they can inject a serialized object payload that causes PHP to invoke arbitrary __wakeup() or __destruct() gadgets the moment the cache entry is read.
How it works
The set() method serializes the value and writes it as a PHP file that returns a plain array:
// system/core/cachefiles.php
public function set($key, $value, $ttl) {
$data = [
'ttl' => $ttl,
'time' => time(),
'value' => serialize($value)
];
$file_path_tmp = $file_path . '.tmp';
$success = file_put_contents($file_path_tmp,
'<?php return ' . var_export($data, true) . ';');
// ...
}
The get() method pulls the file in with include and passes the stored string straight to unserialize():
public function get($key) {
$data = include $file;
// ...
return unserialize($data['value']); // sink
}
There is no allowed_classes argument, so PHP will reconstruct whatever object the serialized string describes and automatically fire any magic methods on it.
Root Cause
The unserialize() call at the read path places no restriction on instantiable classes. This is the canonical prerequisite for PHP Object Injection. The missing argument is ['allowed_classes' => false], which would cause PHP to refuse to rebuild any object and return false instead of executing attacker-controlled code.
Prerequisites
This is a second-order vulnerability. The attacker needs a write primitive into the cache directory first. Realistic paths to that include:
- A path traversal bug elsewhere in iCMS2
- Symlink attacks on a shared hosting environment
- Direct filesystem access from misconfigured permissions or a compromised FTP account
- Any arbitrary file write vulnerability in another installed component
Once the cache file is poisoned, exploitation is automatic: the next request that reads the affected cache key triggers the payload.
Fix
The fix is a one-line change in get():
// Before (vulnerable)
return unserialize($data['value']);
// After (safe for data caches)
return unserialize($data['value'], ['allowed_classes' => false]);
Passing ['allowed_classes' => false] tells PHP to refuse object instantiation during deserialization. A gadget payload still parses, but no object is ever built and no magic method fires.
A stronger long-term approach is to drop PHP serialization entirely for cache values and use JSON:
// Writing
'value' => json_encode($value)
// Reading
return json_decode($data['value'], true);
json_decode() never produces PHP objects, which eliminates this entire class of attack regardless of what gadgets exist in the codebase.
Key Takeaways
- Any installation using the
cmsCacheFiles backend with a writable cache directory is potentially vulnerable when combined with a write primitive.
- The worst-case outcome is remote code execution with full database access and the ability to drop a persistent webshell.
- The fix is a single-argument change to
unserialize(), or a full migration to JSON serialization for cache values.
- Treat every file the application reads from disk as untrusted if its write path is not tightly controlled.