Roundcube Webmail(CVE-2025-49113)认证后php反序列化rce复现新视角
1674701160110592 发表于 广东 WEB安全 1707浏览 · 2025-06-09 14:26

信息收集

找到RoundCube的commit





这里发现几处文件修改的位置都是对program/actions/settings/upload.php这个文件来进行更改的,同时还对type这个参数进行了一定的限制

下载源码,这里我用的是1.5.9的版本





代码审计

修改的位置在program/actions/settings/upload.php



rcube_utils::INPUT_GET就是传了一个值为1

跟进rcube_utils::get_input_value方法



发现做了&运算,其实就是比较,看看是要求坐get、post、cookie哪个传参

然后调用parse_input_value方法,简单来说就是把传入的值返回出来,最后赋给了$from变量



然后

这个type变量只是简单的正则匹配一些关键字,具体的作用就是把$from传入的内容的add-或者edit-后面的内容给提取出来

或者你也可以说是把add-或者edit-删掉

然后下面的那句就是替换.为-。

例如:





我们查一下这个$type变量最后去到哪里

最终发现走到这两个函数里面

$rcmail->config->get

我们通过正常传参可以发现



$rcmail->config->get这个函数是获取identity

$type . '_image_size'拼接之后的值就是identity_image_size



应该就是为了获取最大的大小值

$rcmail->session->append

那么我们重点关注append方法



发现他是从$_SESSION这个数据获取节点,然后以键值对的形式进行赋值,

或者说是给$_SESSION给这个数据进行添加值

最后把已经存在的键给他删掉

那么经常关注php会话(session)漏洞机制的话,不难会想到跟序列化和反序列化有关

自定义反序列化方法

从这个反序列化开始看起



发现它不是 PHP 自带的标准 unserialize() 的替代品,而是一个“适配特定格式的解析器”,

主要负责把输入的字符串转换为 PHP 能够 unserialize() 的格式。



用这个自定义的unserialize函数接收一个字符串 $str,这个字符串是一个自定义的序列化格式(并不是 PHP 默认的序列化格式),它会:

解析这个自定义格式;

拼接出合法的 PHP 序列化字符串;

最后用 PHP 自带的 unserialize() 函数把它变成 PHP 数据结构(如数组、对象等)返回。

开头的代码其实实在定义其实是想定义一个移动指针,并为后面的计算条件、值和名称做准备

主体的两个while循环其实就是为了遍历$str变量,每次取出一段键和值,并将其拼接成合法的 PHP 序列化结构。

具体的过程就是把|作为分割符号,

键的格式是|符号前面的内容,值的格式是|符号后面的内容

值的格式是自定义的: 如: s:3:"abc";(字符串) i:123;(整数) N;(null) a:2:{...}(数组) o:8:"stdClass":...(对象)

例如

最后经过转换就变成了

然后就会把这段代码交给原生的unserialize去处理

也就是最终的漏洞点



当然其中还有一个if判断

这里比较有意思的就是,当碰到键值对的键前面有个感叹号!,他就会把$has_value赋值成false

导致了序列化直接就变成了N;也就是置空的意思。

比如我们传一个



他会把对象里面的属性直接忽略掉设置成null值,同时1;跑到了前面的req_jwt_token对象名里面去了,造成了混乱

如果去掉感叹号!的结果是

还有一个switch分支结构,用来处理各种类型的数据,然后拼接到$serialized变量上面去,也就是我们最终调用的漏洞点



环境搭建

具体参考



访问漏洞点的位置





进行图片上传



找了一下session存储的路径,发现都是空的

这时候想起来可能是存在数据库了

mysql -e "use roundcube;select * from session;"





然后我们发送报文,再查看一下数据库里面的session(最后一栏的vars列,base64解码得到)

bp报文



下面是已经去除了一部分数据剩下的

可以发现cropped.png是我们的一个可控值,也就是文件名字

然后经过测试,

get传参中的_from参数也是我们的可控值。



制作payload

现在我们该思考的是,要序列化哪个类进行RCE或者其他利用的操作呢?

我们可以将任意变量注入会话。但是,如果有一个参数的字符串值被强制包含编码的序列化字符串,该怎么办?

也就是说,该值只是一个字符串,rcube_session→unserialize()会将其简单地赋值给一个变量,

然后,这些数据稍后会被传递给另一个unserialize() 函数?幸运的是,这样的代码确实存在。

搜寻反序列化入口

在一番fuzz之后找到了能传入指定反序列化的入口

program\lib\Roundcube\rcube_user.php





在\program\lib\Roundcube\rcube_user.php,他是直接获取了session数组里面的preferences键的值

然后直接进行反序列化的。

看到这个session->remove

反序列化命令执行的同时还把记录都给删掉了,也许这就是这个漏洞难以发现的原因之一

搜寻恶意类

那么我们现在还缺少的是一个恶意类

在这里,Roundcube 没有让人失望,它有一个来自PEAR库的精彩类可用

在./vendor/pear/crypt_gpg/Crypt/GPG/Engine.php文件下面有个__closeIdleAgents私有方法可以进行命令拼接,然后导致执行命令





经过反复的fuzz发现_from传入的内容会产生分隔符,也就是说它可以经过Roundcube自己的反序列化生成一个自定义的恶意反序列化



get参数中的_from传入Crypt_GPG_Engine类,然后文件名字传入preferences让他传给unserialize反序列化进行命令执行的触发比较合适





上面的payload经过Roundcube自己的反序列化就会变成

我们分段来看

其实这个过程就是有点像字符串逃逸了

而实际上bp报文就是

这里也许你会好奇_from=edit-为什么要传入!";呢?



这里实际上我传入的是edit-!";identity,你可以直观的感受到";identity的值直接就设置成空了,后面文件名的注入点也没了

实际上已经是执行了反序列化了,这里加一个感叹号将用于销毁对象,类似于快速销毁,但仅用于确保有效载荷不会保存在数据库中,并且不会重复执行。仅执行一次,之后具有相同键的变量会将此对象从数组中删除。



绕过空字节

我们都知道私有变量是有个空字符的,而Roundcube存在着waf

我们可以利用PHPGGC的process_serialized函数来进行绕过

具体如下



复现

运行





至此复现完毕

总结

该漏洞的反序列化点非常值得学习,也算是锻炼了自己代码审计以及漏洞复现的能力。





参考链接

https://fearsoff.org/research/roundcube

0 条评论
某人
表情
可输入 255