前言
学弟在 Vulhub 里看到了这个漏洞 , 转发给我让我解释一下原理 . 我哪会这玩意 ... 不过为了不丢脸 , 只好硬着头皮看一下 .
漏洞原理不难 , 但是分析过程和网上资料还是非常有意思的 , 这里记录一下 ~
漏洞复现条件
NVD( 国家漏洞数据库 ) 上给出的漏洞应用对应版本为 Webmin <= 1.920
. 但这并不是完全正确的 . 准确的说 , 只有在 Sourceforge 站点上下载的版本低于 1.920
的 Webmin
应用才会存在远程代码执行漏洞 , 而在官方 Github 上对应版本的 Webmin 应用是不存在该漏洞的 .
但不幸的是 , 官方网站上的下载链接是默认指向 Sourceforge
的 .
这就非常有趣了 , 为什么同一版本的应用在两个网站上的发行版内容会不相同呢? 而且这还是在两个网站都是官方维护的情况下 . 带着这个疑惑 , 我们开始分析这个漏洞 .
CVE-2019-15107
Webmin 介绍
先来简单的介绍一下 Webmin 是什么吧 .
Webmin是一个简化Linux或Unix系统管理过程的程序。通常,您需要手动编辑配置文件并运行命令来创建帐户,设置Web服务器和管理电子邮件转发。 Webmin使您可以通过易于使用的Web界面执行这些任务以及更多任务,并自动为您更新所有必需的配置文件。这使得管理系统的工作变得更加容易。
这是 官方文档 上的原话 . 从简述中不难看出 , 该应用是一个用于管理 Linux/Unix 系统 的GUI Web 程序 . 通过图形化的操作方式来减少和简化运维人员的工作 .
该应用的界面是下面这样的 , 还是比较清晰明了的 .
漏洞环境搭建
我们选用 Vulhub 上的 Docker 容器作为漏洞复现和研究学习的环境 .
该容器中使用 Webmin 1.910 作为漏洞环境 , 刚好符合我们的需求 .
然后运行 sudo docker-compose -f docker-compose.yml up
即可安装并运行环境
访问 https://127.0.0.1:10001
即可看到 Web 登录页面 . 如果提示 "Potential Security Risk Ahead"
, 自行 Accept
一下就可以了 .
漏洞利用复现
虽然该漏洞的利用条件是不需要任何权限的 , 但为了方便您理解 , 我这里还是按照用户操作顺序来 .
-
首先我想要登录 Webmin . 登录 Webmin 可以通过 Webmin 账户或者系统账户 , 但目前我不知道任何一个账户 , 所以我们需要进入 docker 环境来找到一个可用于登录的用户
sudo docker exec -it cve-2019-15107_web_1 /bin/bash
登录后直接成为 root 用户 , 那我们就使用 root 用户来登录 Webmin . 我们可以通过
Webmin
内置的change-passwd.pl
文件来修改 root 登录用户的密码 . 这里将密码设置为 " 1234 "然后我们就可以使用
root/1234
这个用户登录Webmin
了 . -
创建一个新用户 , 并跳转到修改密码页面
通过查看 NVD 我们知道 , 该漏洞产生的位置位于
password_change.cgi
. 那么在正常情况下如何跳转到该页面呢 ? 我们需要新增一个用户 , 然后设置该用户在下次登录时必须更改密码 .按照如图顺序添加一个新用户
添加新用户时注意勾选
Force change at next login
按钮创建用户完毕后 , 我们退出当前登录用户( root ) , 然后以新用户( test ) 登录 , 即可跳转到修改密码的页面
session_login.cgi
. 该页面的提交页面是password_change.cgi
------ 即漏洞存在的页面 . 我们打开BurpSuite
, 开启抓包功能 , 随意填写页面后提交 , 并将捕获到的 POST 数据包放到Repeater
模块 . -
修改 Payload 并利用该漏洞
可以看到这里我们成功修改了密码 , 我们修改 POST 数据包如下所示 . 并再次提交 .
Payload :
user=test&pam=1&expired=2&old=asd|id&new1=test&new2=test
成功复现该漏洞 !
如果您是直接访问 password_change.cgi
页面进行漏洞利用 , 则极有可能漏洞利用不成功 . 解决方法有以下两种 .
-
HTTP Referer未正确配置
如果您出现如下返回页面 , 则极有可能是因为未设置
HTTP Referer
.您当然可以按照页面给出的解决方案修改相关配置 , 不过这非常麻烦 . 通过正常的执行流程不难发现 , 这里需要添加
Referer: https://127.0.0.1:10001/session_login.cgi
这个键值对作为HTTP Referer
, 直接添加后即可利用成功 . -
密码修改功能未开启
如果您出现如下返回页面 , 则极有可能未开启密码修改功能 .
这里需要我们修改
passwd_mode
参数的值 , 我们再次进入 docker 容器中 , 对/etc/webmin/miniserv.conf
进行相关操作 .将
passwd_mode
修改为 " 2 " , 再次发送数据包就可以利用成功 . 无需重启服务 .顺带一提哈 , sed 的用法也是我最近才看得 , 虽然不是最实用的修改文件方式 , 但绝对是最酷的方法哈哈
漏洞源码分析
下面来分析一下漏洞产生的原因 . 我们再次进入到 docker 容器中 , 下面的大部分操作都需要直接修改 WebMin
源代码 . 我们直接定位到漏洞文件 .
打开目标文件后即可看到 Perl 编写的 CGI 程序代码 . 我有了解过 Perl 这门语言 , 可惜 学习笔记 只写了一篇 , 可以作为参考 . 当然这里的代码并不难 , 有点代码基础的师傅可以直接参考 W3CSchool 或者 RunNoob
注释解释的比较清楚 , 该文件通过直接修改 /etc/shadow
文件来更新用户的登录密码 . 这类涉即系统文件的操作本身就存在比较高的风险 , 下面我们来看一看它的代码逻辑是怎么样的 .
-
判断
passwd_mode
参数的值一些初始化和导入操作我们就不看了 , 这里最关键的点就是对
passwd_mode
参数值的判断 . 如果该参数值不为 " 2 " , 那么代码会直接抛出错误并返回 . 这也正是对应之前修改passwd_mode
的操作 .而在图形化界面 , 该参数对应用户是否可以修改过期密码的操作 , 而值 " 2 " 就是允许用户修改密码 , 这样之前的逻辑也都说的通了 .
-
判断新密码是否有效
既然这里是更改密码的操作 , 那么肯定要判断新密码是否有效 . 这两行代码逻辑非常清楚 , 可以猜到
$in{}
是从HTTP POST
方法获取到的参数 Hash ( 为啥叫它 Hash 请您自己查阅 Perl 语法 ) , 如果还有疑问 , 可以通过Data::Dumper()
函数查看in
变量的内容 .Data::Dumper 模块可以输出复杂嵌套的数据结构 Perl 中 Hash 返回的键值对是乱序的 , 所以不必因多次返回结果不同而感到惊讶 .
-
判断该用户是否为
WebMin
用户首先 , 该文件从外界导入了
acl-lib.pl
这个文件 , 该文件由俩单词组成 , ACL 是访问控制列表 , Lib 是库 , 看名字也能猜到这个文件的功能 .然后通过
Grep
语法将值赋给列表wuser . 这里要来看一看Grep
语法Grep 会对原列表里的每个元素进行匹配 . 它遍历列表 , 并临时设置元素为 $_ . 在列表上下文里 , Grep 返回匹配命中的所有元素 , 返回值也是个列表 . 在标量上下文里 , Grep 返回匹配命中的元素个数 .
有关更详细的内容可以参考 Perl中 Grep 用法总结 . 而我们现在最想知道的是 : 原列表的内容是什么 . 因此通过
die + Dumper
来输出并分析 .可以看到 ,
acl::list_users
中包含了所有可登录的用户( 最初的root
和刚才创建的test
) . 而根据代码逻辑 , 不难判断出这里是判断当前登录用户是否存在($_->['name']
为所有可登录的用户名 ,$in{'name'}
为 POST 方法提交的用户名 ) , 然后将结果返回给($wuser)
变量 .然后会判断当前登录用户( 上一步被赋值在
($wuser)
变量中 )的pass
字段是是否为" x "
. 如果是就把$wuser
变量设置为undef
. 我们再来关注一下pass
字段的值可以看到 ,
root
用户的pass
字段为" x "
, 而test
用户的pass
字段为加密字符串 . 不难判断出 , 如果当前用户为系统登录用户(root
) , 就将$wuser
的值改为undef
. -
判断密码配置文件是否存在
这一步主要是判断密码配置文件是否存在 . 不过进入该判断语句的条件是 HTTP POST 方法传输的
name
字段为系统登录用户(root
) , 且pam
字段的值为空 . 这在正常流程和复现流程中是不会出现的 , 因此这里不是重点 . -
根据
$wuser
字段的值判断当前用户是否可以修改密码 .这里会对
$wuser
字段的值进行判断 , 如果不为空 , 就允许该用户修改密码 .这里需要再说明一下 undef 的含义 , undef 表示缺失一个值 , 也就是其它语言中的空值 . 在数值操作中使用 undef , 它的值等同于 " 0 " . 在字符串操作中使用 undef , 它的值等同于空字符串 .
那么到底哪些用户可以修改密码呢 ? 我们再通过
die + Dumper
格式语句来输出此时$wuser
变量的信息 .因此 , 只要这里的登录用户不为系统登录用户 , 那么就可以进入到下面的条件判断语句中 . 这也证实了该利用点的另一个条件 : 修改密码的用户不能为系统登录用户 .
而令人诧异的是 , 当不存在的用户想要修改密码时 , 也可以进入下面的条件判断语句的 ! 这是非常不符合常规逻辑( 一般情况下 , 会先判断想要修改密码的用户是否存在 ) . 因此 , 下面条件判断语句中的代码不需要任何授权 , 任何用户都能访问 .
这是程序员的疏忽还是有什么不可告人的目的? 我们继续来看代码 .
-
判断原密码是否正确
然后对用户在 POST 提交的原密码进行加密 , 然后和之前存储的加密过的旧用户密码进行对比 , 如果相同 , 就继续执行下面步骤 . 如果不相同 , 就提示用户原密码不正确 .
这个逻辑是完全没有问题的 , 但是注意在这一步之后 , 还有一小段代码 !
-
qx/$in{'old'}/)
qx EXPR qx 用于替代使用反引号来执行系统命令。 例如,qw(ls -l)将执行 UNIX 的 ls 命令使用 -l 命令行选项 . 实际上 , 这里可以使用任何分隔符 , 而不仅仅是括号组 . qx 的返回值为系统命令执行的结果
也就是说 ,
qx/$in{'old'}/)
等同于通过系统执行HTTP POST
里old
参数对应的值 . 当我们输入 old = asd | id 时 , 系统会执行 id 命令并返回结果 .并且 , 这里的远程代码执行不需要任何权限 . 任何人都可以利用该漏洞直接在目标服务器上执行系统命令 .
至此 , 整个漏洞利用的原理就已经全部分析完毕了 .
-
幕后黑手
如果您认真看完上面的内容 , 您一定会感觉很困惑 , 这个判断语句的代码逻辑非常奇怪 .
-
在用户修改密码前未有效的判断用户是否存在 .
-
判断原密码是否正确时 , 莫名其妙的添加了一段没有任何意义的高危代码
-
这两个点一结合 , 就可以使任何用户在非授权的情况下执行系统代码 , 拿到 RCE
越看越有疑问 , 这也太巧了吧 ! 这段系统执行的代码怎么看怎么像是故意加上去的 . 没有任何存在的意义 . 然后您肯定回去查询代码历史提交记录 , 以及 Google 相关问题 , 最后你会得到一个惊人的答案 :
-
这个漏洞是人为产生的 ! 是黑客在代码中添加的后门代码 !
-
而且 , 黑客曾不止一次的添加后门代码 !
-
黑客曾两次添加后门代码 , 早在 WebMin_v1.890 , 就已经添加过一次后门 !
在 The stories behind Webmin CVE-2019–15107 这篇文章中 , 就有师傅指出这个点 , 并且拿出了充足的证据 .
我们跳转到 WebMin_v1.890 On Sourceforge 中 password_change.cgi
的第 12
行 .
完全相同的攻击手法 ! 只不过在后面的发行版中程序员将这段代码修改为对密码修改权限的判断 , 因此攻击者也改变了后门的位置 .
在 SeeBug 上的 Webmin(CVE-2019-15107) 远程代码执行漏洞之 backdoor 探究 一文中也指出 , 这是某黑客组织的恶意入侵 . 并且还进行了 Sourceforge
和 Github
上源码的对比 , 我在这里也借用一下 .
在 Github Issue 上同样有师傅提出这个问题
各种证据表明 : 这是黑客对 SourceForge
上 WebMin
发行版植入的后门代码 ! 其实在 2012 年 , 韩国 SourgeForge CDN 节点就曾经被黑客入侵 , 热门应用 phpMyAdmin 被植入后门 . CVE编号 : CVE-2012-5159
. 详细内容可以参考 NVD .
SourgeForge 上存在后门的 WebMin 发行版本如下所示 :
总结
该漏洞的原理还是比较简单的 , 但其背后的故事将会非常有趣 . 十分感谢各位研究该漏洞的师傅给出的思路 .
Exploit
我还写了一个用于漏洞利用的脚本 , 放在 Github 上 , 有需要的师傅可以参考一下 .