在具体分析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版本的源码
1 | https://www.drupal.org/project/drupal/releases/7.31 |
使用MAMP Pro搭建站点后,更改数据库的相关配置:
访问本地IP:8080端口,使用默认安装配置即可。
验证安装成功:
在不登录的情况下,验证payload:
1 | POST /?q=node&destination=node HTTP/1.1 |
我们可以从上面这张图中看出,漏洞的触发点是在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版本的源码
1 | https://www.drupal.org/project/drupal/releases/8.3.0 |
使用MAMP Pro集成环境搭建,更改php.ini配置,打开Yaml扩展:
查看PHPINFO验证是否开启Yaml扩展:
必须在配置文件中启用yaml.decode_php,否则无法复现成功。
1 | yaml.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:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";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/object
tag,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
更容易利用。
/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
通过反序列化这个类可以造成写入webshell,但是利用过程相比后面两个而言更为麻烦一点。PHPGGC已经包含了这个gadget,拿过来稍微改一下
1 |
|
/vendor/symfony/process/Pipes/WindowsPipes.php
反序列化这个类可以造成任意文件删除。
/vendor/guzzlehttp/psr7/src/FnStream.php
反序列化这个类可以实现无参数RCE。
1 |
|
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版本的源码
1 | https://www.drupal.org/project/drupal/releases/8.5.0 |
使用MAMP Pro搭建,成功安装后,在不登录的情况下发送如下数据包:
1 | POST /user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1 |
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/
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 |
|
然后再编写python脚本生成一个压缩文件,其中被压缩的文件名是PHAR://exploit.phar
,input_file.txt文件内容随意:
1 | import tarfile |
最后编写触发漏洞的代码:
1 |
|
运行上面代码后,可以发现ca01h_test
文件被删除。
接下来再讨论一下CVE-2020-28948,产生漏洞的原因同时是因为过滤不严,只是触发漏洞的位置不一样而言。
程序在第2151行或2158行调用了fwrite
函数,将从压缩文件读出来的文件内容写入到$v_header[filename]
文件中,那么这个地方就可能造成任意文件覆盖的漏洞。流程如下:
首先生成一个测试文件,内容随意:
1 | echo "test" > /tmp/target_file |
再用python脚本生成带有恶意payload文件名的压缩文件:
1 | import tarfile |
最后执行同样的漏洞触发代码:
1 |
|
https://github.com/vulhub/vulhub/tree/master/drupal