origamindee 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +89 -0
- data/COPYING.LESSER +165 -0
- data/README.md +131 -0
- data/bin/config/pdfcop.conf.yml +236 -0
- data/bin/pdf2pdfa +87 -0
- data/bin/pdf2ruby +333 -0
- data/bin/pdfcop +476 -0
- data/bin/pdfdecompress +97 -0
- data/bin/pdfdecrypt +91 -0
- data/bin/pdfencrypt +113 -0
- data/bin/pdfexplode +223 -0
- data/bin/pdfextract +277 -0
- data/bin/pdfmetadata +143 -0
- data/bin/pdfsh +12 -0
- data/bin/shell/console.rb +128 -0
- data/bin/shell/hexdump.rb +59 -0
- data/bin/shell/irbrc +69 -0
- data/examples/README.md +34 -0
- data/examples/attachments/attachment.rb +38 -0
- data/examples/attachments/nested_document.rb +51 -0
- data/examples/encryption/encryption.rb +28 -0
- data/examples/events/events.rb +72 -0
- data/examples/flash/flash.rb +37 -0
- data/examples/flash/helloworld.swf +0 -0
- data/examples/forms/javascript.rb +54 -0
- data/examples/forms/xfa.rb +115 -0
- data/examples/javascript/hello_world.rb +22 -0
- data/examples/javascript/js_emulation.rb +54 -0
- data/examples/loop/goto.rb +32 -0
- data/examples/loop/named.rb +33 -0
- data/examples/signature/signature.rb +65 -0
- data/examples/uri/javascript.rb +56 -0
- data/examples/uri/open-uri.rb +21 -0
- data/examples/uri/submitform.rb +47 -0
- data/lib/origami/3d.rb +364 -0
- data/lib/origami/acroform.rb +321 -0
- data/lib/origami/actions.rb +318 -0
- data/lib/origami/annotations.rb +711 -0
- data/lib/origami/array.rb +242 -0
- data/lib/origami/boolean.rb +90 -0
- data/lib/origami/catalog.rb +418 -0
- data/lib/origami/collections.rb +144 -0
- data/lib/origami/compound.rb +161 -0
- data/lib/origami/destinations.rb +252 -0
- data/lib/origami/dictionary.rb +192 -0
- data/lib/origami/encryption.rb +1084 -0
- data/lib/origami/extensions/fdf.rb +347 -0
- data/lib/origami/extensions/ppklite.rb +422 -0
- data/lib/origami/filespec.rb +197 -0
- data/lib/origami/filters/ascii.rb +211 -0
- data/lib/origami/filters/ccitt/tables.rb +267 -0
- data/lib/origami/filters/ccitt.rb +357 -0
- data/lib/origami/filters/crypt.rb +38 -0
- data/lib/origami/filters/dct.rb +54 -0
- data/lib/origami/filters/flate.rb +69 -0
- data/lib/origami/filters/jbig2.rb +57 -0
- data/lib/origami/filters/jpx.rb +47 -0
- data/lib/origami/filters/lzw.rb +170 -0
- data/lib/origami/filters/predictors.rb +292 -0
- data/lib/origami/filters/runlength.rb +129 -0
- data/lib/origami/filters.rb +364 -0
- data/lib/origami/font.rb +196 -0
- data/lib/origami/functions.rb +79 -0
- data/lib/origami/graphics/colors.rb +230 -0
- data/lib/origami/graphics/instruction.rb +98 -0
- data/lib/origami/graphics/path.rb +182 -0
- data/lib/origami/graphics/patterns.rb +174 -0
- data/lib/origami/graphics/render.rb +62 -0
- data/lib/origami/graphics/state.rb +149 -0
- data/lib/origami/graphics/text.rb +225 -0
- data/lib/origami/graphics/xobject.rb +918 -0
- data/lib/origami/graphics.rb +38 -0
- data/lib/origami/header.rb +75 -0
- data/lib/origami/javascript.rb +713 -0
- data/lib/origami/linearization.rb +330 -0
- data/lib/origami/metadata.rb +172 -0
- data/lib/origami/name.rb +135 -0
- data/lib/origami/null.rb +65 -0
- data/lib/origami/numeric.rb +181 -0
- data/lib/origami/obfuscation.rb +245 -0
- data/lib/origami/object.rb +760 -0
- data/lib/origami/optionalcontent.rb +183 -0
- data/lib/origami/outline.rb +54 -0
- data/lib/origami/outputintents.rb +85 -0
- data/lib/origami/page.rb +722 -0
- data/lib/origami/parser.rb +269 -0
- data/lib/origami/parsers/fdf.rb +56 -0
- data/lib/origami/parsers/pdf/lazy.rb +176 -0
- data/lib/origami/parsers/pdf/linear.rb +122 -0
- data/lib/origami/parsers/pdf.rb +118 -0
- data/lib/origami/parsers/ppklite.rb +57 -0
- data/lib/origami/pdf.rb +1108 -0
- data/lib/origami/reference.rb +134 -0
- data/lib/origami/signature.rb +702 -0
- data/lib/origami/stream.rb +705 -0
- data/lib/origami/string.rb +444 -0
- data/lib/origami/template/patterns.rb +56 -0
- data/lib/origami/template/widgets.rb +151 -0
- data/lib/origami/trailer.rb +190 -0
- data/lib/origami/tree.rb +62 -0
- data/lib/origami/version.rb +23 -0
- data/lib/origami/webcapture.rb +100 -0
- data/lib/origami/xfa/config.rb +453 -0
- data/lib/origami/xfa/connectionset.rb +146 -0
- data/lib/origami/xfa/datasets.rb +49 -0
- data/lib/origami/xfa/localeset.rb +42 -0
- data/lib/origami/xfa/package.rb +59 -0
- data/lib/origami/xfa/pdf.rb +73 -0
- data/lib/origami/xfa/signature.rb +42 -0
- data/lib/origami/xfa/sourceset.rb +43 -0
- data/lib/origami/xfa/stylesheet.rb +44 -0
- data/lib/origami/xfa/template.rb +1691 -0
- data/lib/origami/xfa/xdc.rb +42 -0
- data/lib/origami/xfa/xfa.rb +146 -0
- data/lib/origami/xfa/xfdf.rb +43 -0
- data/lib/origami/xfa/xmpmeta.rb +43 -0
- data/lib/origami/xfa.rb +62 -0
- data/lib/origami/xreftable.rb +557 -0
- data/lib/origami.rb +47 -0
- data/test/dataset/calc.pdf +85 -0
- data/test/dataset/crypto.pdf +36 -0
- data/test/dataset/empty.pdf +49 -0
- data/test/test_actions.rb +27 -0
- data/test/test_annotations.rb +68 -0
- data/test/test_forms.rb +30 -0
- data/test/test_native_types.rb +83 -0
- data/test/test_object_tree.rb +33 -0
- data/test/test_pages.rb +60 -0
- data/test/test_pdf.rb +20 -0
- data/test/test_pdf_attachment.rb +34 -0
- data/test/test_pdf_create.rb +24 -0
- data/test/test_pdf_encrypt.rb +102 -0
- data/test/test_pdf_parse.rb +134 -0
- data/test/test_pdf_parse_lazy.rb +69 -0
- data/test/test_pdf_sign.rb +97 -0
- data/test/test_streams.rb +184 -0
- data/test/test_xrefs.rb +67 -0
- metadata +280 -0
data/bin/pdfcop
ADDED
@@ -0,0 +1,476 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
= Info
|
6
|
+
This is a PDF document filtering engine using Origami.
|
7
|
+
Security policies are based on a white list of PDF features.
|
8
|
+
Default policies details can be found in the default configuration file.
|
9
|
+
You can also add your own policy and activate it using the -p switch.
|
10
|
+
|
11
|
+
= License
|
12
|
+
Copyright (C) 2016 Guillaume Delugré.
|
13
|
+
|
14
|
+
Origami is free software: you can redistribute it and/or modify
|
15
|
+
it under the terms of the GNU Lesser General Public License as published by
|
16
|
+
the Free Software Foundation, either version 3 of the License, or
|
17
|
+
(at your option) any later version.
|
18
|
+
|
19
|
+
Origami is distributed in the hope that it will be useful,
|
20
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
21
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
22
|
+
GNU Lesser General Public License for more details.
|
23
|
+
|
24
|
+
You should have received a copy of the GNU Lesser General Public License
|
25
|
+
along with Origami. If not, see <http://www.gnu.org/licenses/>.
|
26
|
+
|
27
|
+
=end
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'origami'
|
31
|
+
rescue LoadError
|
32
|
+
$: << File.join(__dir__, '../lib')
|
33
|
+
require 'origami'
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'optparse'
|
37
|
+
require 'yaml'
|
38
|
+
require 'rexml/document'
|
39
|
+
require 'digest/sha2'
|
40
|
+
require 'fileutils'
|
41
|
+
require 'rainbow'
|
42
|
+
|
43
|
+
DEFAULT_CONFIG_FILE = "#{File.dirname(__FILE__)}/config/pdfcop.conf.yml"
|
44
|
+
DEFAULT_POLICY = "standard"
|
45
|
+
SECURITY_POLICIES = {}
|
46
|
+
ANNOTATION_RIGHTS = {
|
47
|
+
FileAttachment: %i[allowAttachments allowFileAttachmentAnnotation],
|
48
|
+
Sound: %i[allowSoundAnnotation],
|
49
|
+
Movie: %i[allowMovieAnnotation],
|
50
|
+
Screen: %i[allowScreenAnnotation],
|
51
|
+
Widget: %i[allowAcroforms],
|
52
|
+
RichMedia: %i[allowRichMediaAnnotation],
|
53
|
+
:"3D" => %i[allow3DAnnotation]
|
54
|
+
}
|
55
|
+
|
56
|
+
|
57
|
+
def load_config_file(path)
|
58
|
+
SECURITY_POLICIES.update(Hash.new(false).update YAML.load(File.read(path)))
|
59
|
+
end
|
60
|
+
|
61
|
+
class OptParser
|
62
|
+
BANNER = <<USAGE
|
63
|
+
Usage: #{$0} [options] <PDF-file>
|
64
|
+
The PDF filtering engine. Scans PDF documents for malicious structures.
|
65
|
+
Bug reports or feature requests at: http://github.com/gdelugre/origami
|
66
|
+
|
67
|
+
Options:
|
68
|
+
USAGE
|
69
|
+
|
70
|
+
def self.parse(args)
|
71
|
+
options = {colors_enabled: true, password: ''}
|
72
|
+
|
73
|
+
parser = OptionParser.new do |opts|
|
74
|
+
opts.banner = BANNER
|
75
|
+
|
76
|
+
opts.on("-o", "--output LOG_FILE", "Output log file (default STDOUT)") do |o|
|
77
|
+
options[:output_log] = o
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on("-c", "--config CONFIG_FILE", "Load security policies from given configuration file") do |cf|
|
81
|
+
options[:config_file] = cf
|
82
|
+
end
|
83
|
+
|
84
|
+
opts.on("-p", "--policy POLICY_NAME", "Specify applied policy. Predefined policies: 'none', 'standard', 'strong', 'paranoid'") do |policy|
|
85
|
+
options[:policy] = policy
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on("-m", "--move PATH", "Move rejected documents to the specified directory.") do |dir|
|
89
|
+
options[:move_dir] = dir
|
90
|
+
end
|
91
|
+
|
92
|
+
opts.on("-P", "--password PASSWORD", "Password to use if the document is encrypted") do |passwd|
|
93
|
+
options[:password] = passwd
|
94
|
+
end
|
95
|
+
|
96
|
+
opts.on("-n", "--no-color", "Turn off colorized output") do
|
97
|
+
options[:colors_enabled] = false
|
98
|
+
end
|
99
|
+
|
100
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
101
|
+
puts opts
|
102
|
+
exit
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
parser.parse!(args)
|
107
|
+
|
108
|
+
options
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
@options = OptParser.parse(ARGV)
|
113
|
+
if @options.key?(:output_log)
|
114
|
+
LOGGER = File.open(@options[:output_log], "a+")
|
115
|
+
else
|
116
|
+
LOGGER = STDOUT
|
117
|
+
end
|
118
|
+
|
119
|
+
if not @options.key?(:policy)
|
120
|
+
@options[:policy] = DEFAULT_POLICY
|
121
|
+
end
|
122
|
+
|
123
|
+
if @options.key?(:move_dir) and not File.directory?(@options[:move_dir])
|
124
|
+
abort "Error: #{@options[:move_dir]} is not a valid directory."
|
125
|
+
end
|
126
|
+
|
127
|
+
Rainbow.enabled = @options[:colors_enabled]
|
128
|
+
|
129
|
+
load_config_file(@options[:config_file] || DEFAULT_CONFIG_FILE)
|
130
|
+
unless SECURITY_POLICIES.key?("POLICY_#{@options[:policy].upcase}")
|
131
|
+
abort "Undeclared policy `#{@options[:policy]}'"
|
132
|
+
end
|
133
|
+
|
134
|
+
if ARGV.empty?
|
135
|
+
abort "Error: No filename was specified. #{$0} --help for details."
|
136
|
+
else
|
137
|
+
TARGET = ARGV.shift
|
138
|
+
end
|
139
|
+
|
140
|
+
def log(message, color = :default)
|
141
|
+
time_txt = Rainbow("[#{Time.now}]").cyan
|
142
|
+
msg_txt = Rainbow(message).color(color)
|
143
|
+
LOGGER.puts("#{time_txt} #{msg_txt}")
|
144
|
+
end
|
145
|
+
|
146
|
+
def reject(cause)
|
147
|
+
log("Document rejected by policy `#{@options[:policy]}', caused by #{cause.inspect}.", :red)
|
148
|
+
|
149
|
+
if @options.key?(:move_dir)
|
150
|
+
quarantine(TARGET, @options[:move_dir])
|
151
|
+
end
|
152
|
+
|
153
|
+
abort
|
154
|
+
end
|
155
|
+
|
156
|
+
def quarantine(file, quarantine_folder)
|
157
|
+
digest = Digest::SHA256.file(file)
|
158
|
+
ext = File.extname(file)
|
159
|
+
dest_name = "#{File.basename(file, ext)}_#{digest}#{ext}"
|
160
|
+
dest_path = File.join(quarantine_folder, dest_name)
|
161
|
+
|
162
|
+
FileUtils.move(file, dest_path)
|
163
|
+
end
|
164
|
+
|
165
|
+
def check_rights(*required_rights)
|
166
|
+
current_rights = SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]
|
167
|
+
|
168
|
+
reject(required_rights) if required_rights.any?{|right| current_rights[right.to_s] == false}
|
169
|
+
end
|
170
|
+
|
171
|
+
def analyze_xfa_forms(xfa)
|
172
|
+
case xfa
|
173
|
+
when Origami::Array then
|
174
|
+
xml = ""
|
175
|
+
i = 0
|
176
|
+
xfa.each do |packet|
|
177
|
+
if i % 2 == 1
|
178
|
+
xml << packet.solve.data
|
179
|
+
end
|
180
|
+
|
181
|
+
i = i + 1
|
182
|
+
end
|
183
|
+
when Origami::Stream then
|
184
|
+
xml = xfa.data
|
185
|
+
else
|
186
|
+
reject("Malformed XFA dictionary")
|
187
|
+
end
|
188
|
+
|
189
|
+
xfadoc = REXML::Document.new(xml)
|
190
|
+
REXML::XPath.match(xfadoc, "//script").each do |script|
|
191
|
+
case script.attributes["contentType"]
|
192
|
+
when "application/x-formcalc" then
|
193
|
+
check_rights(:allowFormCalc)
|
194
|
+
else
|
195
|
+
check_rights(:allowJS)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def check_annotation_rights(annot)
|
201
|
+
subtype = annot.Subtype.value
|
202
|
+
|
203
|
+
check_rights(*ANNOTATION_RIGHTS[subtype]) if ANNOTATION_RIGHTS.include?(subtype)
|
204
|
+
end
|
205
|
+
|
206
|
+
def analyze_annotation(annot, _level = 0)
|
207
|
+
check_rights(:allowAnnotations)
|
208
|
+
|
209
|
+
if annot.is_a?(Origami::Dictionary) and annot.key?(:Subtype)
|
210
|
+
check_annotation_rights(annot)
|
211
|
+
|
212
|
+
analyze_3d_annotation(annot) if annot.Subtype.value == :"3D"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def analyze_3d_annotation(annot)
|
217
|
+
# 3D annotation might pull in JavaScript for real-time driven behavior.
|
218
|
+
return unless annot.key?(:"3DD")
|
219
|
+
|
220
|
+
dd = annot[:"3DD"].solve
|
221
|
+
u3dstream = nil
|
222
|
+
|
223
|
+
case dd
|
224
|
+
when Origami::Stream
|
225
|
+
u3dstream = dd
|
226
|
+
when Origami::Dictionary
|
227
|
+
u3dstream = dd[:"3DD"].solve
|
228
|
+
end
|
229
|
+
|
230
|
+
if u3dstream.is_a?(Stream) and u3dstream.key?(:OnInstantiate)
|
231
|
+
check_rights(:allowJS)
|
232
|
+
|
233
|
+
if annot.key?(:"3DA") # is 3d view instantiated automatically?
|
234
|
+
u3dactiv = annot[:"3DA"].solve
|
235
|
+
|
236
|
+
check_rights(:allowJSAtOpening) if u3dactiv.is_a?(Origami::Dictionary) and (u3dactiv.A == :PO or u3dactiv.A == :PV)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def analyze_page(page, level = 0)
|
242
|
+
section_prefix = " " * 2 * level + ">" * (level + 1)
|
243
|
+
log(section_prefix + " Inspecting page...")
|
244
|
+
|
245
|
+
text_prefix = " " * 2 * (level + 1) + "." * (level + 1)
|
246
|
+
if page.is_a?(Origami::Dictionary)
|
247
|
+
#
|
248
|
+
# Checking page additional actions.
|
249
|
+
#
|
250
|
+
if page.key?(:AA)
|
251
|
+
if page.AA.is_a?(Origami::Dictionary)
|
252
|
+
log(text_prefix + " Page has an action dictionary.")
|
253
|
+
|
254
|
+
aa = Origami::Page::AdditionalActions.new(page.AA); aa.parent = page.AA.parent
|
255
|
+
analyze_action(aa.O, true, level + 1) if aa.key?(:O)
|
256
|
+
analyze_action(aa.C, false, level + 1) if aa.key?(:C)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
#
|
261
|
+
# Looking for page annotations.
|
262
|
+
#
|
263
|
+
page.each_annotation do |annot|
|
264
|
+
analyze_annotation(annot, level + 1)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def analyze_action(action, triggered_at_opening, level = 0)
|
270
|
+
section_prefix = " " * 2 * level + ">" * (level + 1)
|
271
|
+
log(section_prefix + " Inspecting action...")
|
272
|
+
|
273
|
+
text_prefix = " " * 2 * (level + 1) + "." * (level + 1)
|
274
|
+
if action.is_a?(Origami::Dictionary)
|
275
|
+
log(text_prefix + " Found #{action[:S]} action.")
|
276
|
+
type = action[:S].is_a?(Origami::Reference) ? action[:S].solve : action[:S]
|
277
|
+
|
278
|
+
case type.value
|
279
|
+
when :JavaScript
|
280
|
+
check_rights(:allowJS)
|
281
|
+
check_rights(:allowJSAtOpening) if triggered_at_opening
|
282
|
+
|
283
|
+
when :Launch
|
284
|
+
check_rights(:allowLaunchAction)
|
285
|
+
|
286
|
+
when :Named
|
287
|
+
check_rights(:allowNamedAction)
|
288
|
+
|
289
|
+
when :GoTo
|
290
|
+
check_rights(:allowGoToAction)
|
291
|
+
dest = action[:D].is_a?(Origami::Reference) ? action[:D].solve : action[:D]
|
292
|
+
if dest.is_a?(Origami::Array) and dest.length > 0 and dest.first.is_a?(Origami::Reference)
|
293
|
+
dest_page = dest.first.solve
|
294
|
+
if dest_page.is_a?(Origami::Page)
|
295
|
+
log(text_prefix + " Destination page found.")
|
296
|
+
analyze_page(dest_page, level + 1)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
when :GoToE
|
301
|
+
check_rights(:allowAttachments,:allowGoToEAction)
|
302
|
+
|
303
|
+
when :GoToR
|
304
|
+
check_rights(:allowGoToRAction)
|
305
|
+
|
306
|
+
when :Thread
|
307
|
+
check_rights(:allowGoToRAction) if action.key?(:F)
|
308
|
+
|
309
|
+
when :URI
|
310
|
+
check_rights(:allowURIAction)
|
311
|
+
|
312
|
+
when :SubmitForm
|
313
|
+
check_rights(:allowAcroForms,:allowSubmitFormAction)
|
314
|
+
|
315
|
+
when :ImportData
|
316
|
+
check_rights(:allowAcroForms,:allowImportDataAction)
|
317
|
+
|
318
|
+
when :Rendition
|
319
|
+
check_rights(:allowScreenAnnotation,:allowRenditionAction)
|
320
|
+
|
321
|
+
when :Sound
|
322
|
+
check_rights(:allowSoundAnnotation,:allowSoundAction)
|
323
|
+
|
324
|
+
when :Movie
|
325
|
+
check_rights(:allowMovieAnnotation,:allowMovieAction)
|
326
|
+
|
327
|
+
when :RichMediaExecute
|
328
|
+
check_rights(:allowRichMediaAnnotation,:allowRichMediaAction)
|
329
|
+
|
330
|
+
when :GoTo3DView
|
331
|
+
check_rights(:allow3DAnnotation,:allowGoTo3DAction)
|
332
|
+
end
|
333
|
+
|
334
|
+
if action.key?(:Next)
|
335
|
+
log(text_prefix + "This action is chained to another action!")
|
336
|
+
check_rights(:allowChainedActions)
|
337
|
+
analyze_action(action.Next)
|
338
|
+
end
|
339
|
+
|
340
|
+
elsif action.is_a?(Origami::Array)
|
341
|
+
dest = action
|
342
|
+
if dest.length > 0 and dest.first.is_a?(Origami::Reference)
|
343
|
+
dest_page = dest.first.solve
|
344
|
+
if dest_page.is_a?(Origami::Page)
|
345
|
+
log(text_prefix + " Destination page found.")
|
346
|
+
check_rights(:allowGoToAction)
|
347
|
+
analyze_page(dest_page, level + 1)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
begin
|
354
|
+
log("PDFcop is running on target `#{TARGET}', policy = `#{@options[:policy]}'", :green)
|
355
|
+
log(" File size: #{File.size(TARGET)} bytes", :magenta)
|
356
|
+
log(" SHA256: #{Digest::SHA256.file(TARGET)}", :magenta)
|
357
|
+
|
358
|
+
@pdf = Origami::PDF.read(TARGET,
|
359
|
+
verbosity: Origami::Parser::VERBOSE_QUIET,
|
360
|
+
ignore_errors: SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]['allowParserErrors'],
|
361
|
+
decrypt: SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]['allowEncryption'],
|
362
|
+
prompt_password: lambda { '' },
|
363
|
+
password: @options[:password]
|
364
|
+
)
|
365
|
+
|
366
|
+
log("> Inspecting document structure...", :yellow)
|
367
|
+
if @pdf.encrypted?
|
368
|
+
log(" . Encryption = YES")
|
369
|
+
check_rights(:allowEncryption)
|
370
|
+
end
|
371
|
+
|
372
|
+
log("> Inspecting document catalog...", :yellow)
|
373
|
+
catalog = @pdf.Catalog
|
374
|
+
reject("Invalid document catalog") unless catalog.is_a?(Origami::Catalog)
|
375
|
+
|
376
|
+
if catalog.key?(:OpenAction)
|
377
|
+
log(" . OpenAction entry = YES")
|
378
|
+
check_rights(:allowOpenAction)
|
379
|
+
action = catalog.OpenAction
|
380
|
+
analyze_action(action, true, 1)
|
381
|
+
end
|
382
|
+
|
383
|
+
if catalog.key?(:AA)
|
384
|
+
if catalog.AA.is_a?(Origami::Dictionary)
|
385
|
+
aa = Origami::CatalogAdditionalActions.new(catalog.AA); aa.parent = catalog;
|
386
|
+
log(" . Additional actions dictionary = YES")
|
387
|
+
analyze_action(aa.WC, false, 1) if aa.key?(:WC)
|
388
|
+
analyze_action(aa.WS, false, 1) if aa.key?(:WS)
|
389
|
+
analyze_action(aa.DS, false, 1) if aa.key?(:DS)
|
390
|
+
analyze_action(aa.WP, false, 1) if aa.key?(:WP)
|
391
|
+
analyze_action(aa.DP, false, 1) if aa.key?(:DP)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
if catalog.key?(:AcroForm)
|
396
|
+
acroform = catalog.AcroForm
|
397
|
+
if acroform.is_a?(Origami::Dictionary)
|
398
|
+
log(" . AcroForm = YES")
|
399
|
+
check_rights(:allowAcroForms)
|
400
|
+
if acroform.key?(:XFA)
|
401
|
+
log(" . XFA = YES")
|
402
|
+
check_rights(:allowXFAForms)
|
403
|
+
|
404
|
+
analyze_xfa_forms(acroform[:XFA].solve)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
log("> Inspecting JavaScript names directory...", :yellow)
|
410
|
+
if @pdf.each_named_script.any?
|
411
|
+
check_rights(:allowJS)
|
412
|
+
check_rights(:allowJSAtOpening)
|
413
|
+
end
|
414
|
+
|
415
|
+
log("> Inspecting attachment names directory...", :yellow)
|
416
|
+
if @pdf.each_attachment.any?
|
417
|
+
check_rights(:allowAttachments)
|
418
|
+
end
|
419
|
+
|
420
|
+
log("> Inspecting document pages...", :yellow)
|
421
|
+
@pdf.each_page do |page|
|
422
|
+
analyze_page(page, 1)
|
423
|
+
end
|
424
|
+
|
425
|
+
log("> Inspecting document streams...", :yellow)
|
426
|
+
@pdf.each_object.select{|obj| obj.is_a?(Origami::Stream)}.each do |stream|
|
427
|
+
if stream.dictionary.key?(:Filter)
|
428
|
+
filters = stream.Filter
|
429
|
+
filters = [ filters ] if filters.is_a?(Origami::Name)
|
430
|
+
|
431
|
+
if filters.is_a?(Origami::Array)
|
432
|
+
filters.each do |filter|
|
433
|
+
case filter.value
|
434
|
+
when :ASCIIHexDecode
|
435
|
+
check_rights(:allowASCIIHexFilter)
|
436
|
+
when :ASCII85Decode
|
437
|
+
check_rights(:allowASCII85Filter)
|
438
|
+
when :LZWDecode
|
439
|
+
check_rights(:allowLZWFilter)
|
440
|
+
when :FlateDecode
|
441
|
+
check_rights(:allowFlateDecode)
|
442
|
+
when :RunLengthDecode
|
443
|
+
check_rights(:allowRunLengthFilter)
|
444
|
+
when :CCITTFaxDecode
|
445
|
+
check_rights(:allowCCITTFaxFilter)
|
446
|
+
when :JBIG2Decode
|
447
|
+
check_rights(:allowJBIG2Filter)
|
448
|
+
when :DCTDecode
|
449
|
+
check_rights(:allowDCTFilter)
|
450
|
+
when :JPXDecode
|
451
|
+
check_rights(:allowJPXFilter)
|
452
|
+
when :Crypt
|
453
|
+
check_rights(:allowCryptFilter)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
#
|
461
|
+
# TODO: Detect JS at opening in XFA (check event tag)
|
462
|
+
# Check image encoding in XFA ?
|
463
|
+
# Only allow valid signed documents ?
|
464
|
+
# Recursively scan attached files.
|
465
|
+
# On-the-fly injection of prerun JS code to hook vulnerable methods (dynamic exploit detection) ???
|
466
|
+
# ...
|
467
|
+
#
|
468
|
+
|
469
|
+
log("Document accepted by policy `#{@options[:policy]}'.", :green)
|
470
|
+
|
471
|
+
rescue
|
472
|
+
log("An error occurred during analysis: #{$!.class} (#{$!.message})")
|
473
|
+
reject("Analysis failure")
|
474
|
+
ensure
|
475
|
+
LOGGER.close
|
476
|
+
end
|
data/bin/pdfdecompress
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
= Info
|
6
|
+
Uncompresses all binary streams of a PDF document.
|
7
|
+
|
8
|
+
= License
|
9
|
+
Copyright (C) 2016 Guillaume Delugré.
|
10
|
+
|
11
|
+
Origami is free software: you can redistribute it and/or modify
|
12
|
+
it under the terms of the GNU Lesser General Public License as published by
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
14
|
+
(at your option) any later version.
|
15
|
+
|
16
|
+
Origami is distributed in the hope that it will be useful,
|
17
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
19
|
+
GNU Lesser General Public License for more details.
|
20
|
+
|
21
|
+
You should have received a copy of the GNU Lesser General Public License
|
22
|
+
along with Origami. If not, see <http://www.gnu.org/licenses/>.
|
23
|
+
|
24
|
+
=end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'origami'
|
28
|
+
rescue LoadError
|
29
|
+
$: << File.join(__dir__, '../lib')
|
30
|
+
require 'origami'
|
31
|
+
end
|
32
|
+
include Origami
|
33
|
+
|
34
|
+
require 'optparse'
|
35
|
+
|
36
|
+
class OptParser
|
37
|
+
BANNER = <<USAGE
|
38
|
+
Usage: #{$0} [<PDF-file>] [-p <password>] [-o <output-file>]
|
39
|
+
Uncompresses all binary streams of a PDF document.
|
40
|
+
Bug reports or feature requests at: http://github.com/gdelugre/origami
|
41
|
+
|
42
|
+
Options:
|
43
|
+
USAGE
|
44
|
+
|
45
|
+
def self.parser(options)
|
46
|
+
OptionParser.new do |opts|
|
47
|
+
opts.banner = BANNER
|
48
|
+
|
49
|
+
opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
|
50
|
+
options[:output] = o
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
54
|
+
puts opts
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse(args)
|
61
|
+
options =
|
62
|
+
{
|
63
|
+
output: STDOUT,
|
64
|
+
}
|
65
|
+
|
66
|
+
self.parser(options).parse!(args)
|
67
|
+
|
68
|
+
options
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
begin
|
73
|
+
@options = OptParser.parse(ARGV)
|
74
|
+
|
75
|
+
target = (ARGV.empty?) ? STDIN : ARGV.shift
|
76
|
+
params =
|
77
|
+
{
|
78
|
+
verbosity: Parser::VERBOSE_QUIET,
|
79
|
+
}
|
80
|
+
|
81
|
+
pdf = PDF.read(target, params)
|
82
|
+
|
83
|
+
pdf.each_object
|
84
|
+
.select { |obj| obj.is_a?(Stream) }
|
85
|
+
.each { |stream|
|
86
|
+
unless stream.filters.any?{|filter| %i[JPXDecode DCTDecode JBIG2Decode].include?(filter.value) }
|
87
|
+
stream.encoded_data = stream.data
|
88
|
+
stream.dictionary.delete(:Filter)
|
89
|
+
end
|
90
|
+
}
|
91
|
+
|
92
|
+
pdf.save(@options[:output], noindent: true)
|
93
|
+
|
94
|
+
rescue
|
95
|
+
STDERR.puts $!.backtrace.join($/)
|
96
|
+
abort "#{$!.class}: #{$!.message}"
|
97
|
+
end
|
data/bin/pdfdecrypt
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
= Info
|
6
|
+
Decrypts a PDF document.
|
7
|
+
|
8
|
+
= License
|
9
|
+
Copyright (C) 2016 Guillaume Delugré.
|
10
|
+
|
11
|
+
Origami is free software: you can redistribute it and/or modify
|
12
|
+
it under the terms of the GNU Lesser General Public License as published by
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
14
|
+
(at your option) any later version.
|
15
|
+
|
16
|
+
Origami is distributed in the hope that it will be useful,
|
17
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
19
|
+
GNU Lesser General Public License for more details.
|
20
|
+
|
21
|
+
You should have received a copy of the GNU Lesser General Public License
|
22
|
+
along with Origami. If not, see <http://www.gnu.org/licenses/>.
|
23
|
+
|
24
|
+
=end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'origami'
|
28
|
+
rescue LoadError
|
29
|
+
$: << File.join(__dir__, '../lib')
|
30
|
+
require 'origami'
|
31
|
+
end
|
32
|
+
include Origami
|
33
|
+
|
34
|
+
require 'optparse'
|
35
|
+
|
36
|
+
class OptParser
|
37
|
+
BANNER = <<USAGE
|
38
|
+
Usage: #{$0} [<PDF-file>] [-p <password>] [-o <output-file>]
|
39
|
+
Decrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256.
|
40
|
+
Bug reports or feature requests at: http://github.com/gdelugre/origami
|
41
|
+
|
42
|
+
Options:
|
43
|
+
USAGE
|
44
|
+
|
45
|
+
def self.parser(options)
|
46
|
+
OptionParser.new do |opts|
|
47
|
+
opts.banner = BANNER
|
48
|
+
|
49
|
+
opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
|
50
|
+
options[:output] = o
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("-p", "--password PASSWORD", "Password of the document") do |p|
|
54
|
+
options[:password] = p
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
58
|
+
puts opts
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.parse(args)
|
65
|
+
options =
|
66
|
+
{
|
67
|
+
output: STDOUT,
|
68
|
+
password: ''
|
69
|
+
}
|
70
|
+
|
71
|
+
self.parser(options).parse!(args)
|
72
|
+
|
73
|
+
options
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
@options = OptParser.parse(ARGV)
|
79
|
+
|
80
|
+
target = (ARGV.empty?) ? STDIN : ARGV.shift
|
81
|
+
params =
|
82
|
+
{
|
83
|
+
verbosity: Parser::VERBOSE_QUIET,
|
84
|
+
password: @options[:password]
|
85
|
+
}
|
86
|
+
|
87
|
+
PDF.read(target, params).save(@options[:output], decrypt: true, noindent: true)
|
88
|
+
|
89
|
+
rescue
|
90
|
+
abort "#{$!.class}: #{$!.message}"
|
91
|
+
end
|