Skip to content

Commit 8f464e1

Browse files
committed
Land rapid7#8658, Add Gather PDF Authors auxiliary module
2 parents 9cd254c + afc704a commit 8f464e1

File tree

4 files changed

+288
-0
lines changed

4 files changed

+288
-0
lines changed

Gemfile.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ PATH
3131
packetfu
3232
patch_finder
3333
pcaprub
34+
pdf-reader
3435
pg (= 0.20.0)
3536
railties
3637
rb-readline
@@ -69,6 +70,7 @@ PATH
6970
GEM
7071
remote: https://rubygems.org/
7172
specs:
73+
Ascii85 (1.0.2)
7274
actionpack (4.2.8)
7375
actionview (= 4.2.8)
7476
activesupport (= 4.2.8)
@@ -96,6 +98,7 @@ GEM
9698
tzinfo (~> 1.1)
9799
addressable (2.5.1)
98100
public_suffix (~> 2.0, >= 2.0.2)
101+
afm (0.2.2)
99102
arel (6.0.4)
100103
arel-helpers (2.4.0)
101104
activerecord (>= 3.1.0, < 6)
@@ -166,6 +169,7 @@ GEM
166169
grpc (1.4.0)
167170
google-protobuf (~> 3.1)
168171
googleauth (~> 0.5.1)
172+
hashery (2.1.2)
169173
i18n (0.8.4)
170174
jsobfu (0.4.2)
171175
rkelly-remix
@@ -236,6 +240,12 @@ GEM
236240
pcaprub
237241
patch_finder (1.0.2)
238242
pcaprub (0.12.4)
243+
pdf-reader (2.0.0)
244+
Ascii85 (~> 1.0.0)
245+
afm (~> 0.2.1)
246+
hashery (~> 2.0)
247+
ruby-rc4
248+
ttfunk
239249
pg (0.20.0)
240250
pg_array_parser (0.0.9)
241251
postgres_ext (3.0.0)
@@ -338,6 +348,7 @@ GEM
338348
rspec-rerun (1.1.0)
339349
rspec (~> 3.0)
340350
rspec-support (3.6.0)
351+
ruby-rc4 (0.1.5)
341352
ruby_smb (0.0.18)
342353
bindata
343354
rubyntlm
@@ -365,6 +376,7 @@ GEM
365376
thor (0.19.4)
366377
thread_safe (0.3.6)
367378
timecop (0.9.0)
379+
ttfunk (1.5.1)
368380
tzinfo (1.2.3)
369381
thread_safe (~> 0.1)
370382
tzinfo-data (1.2017.2)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
This module downloads PDF files and extracts the author's name from the document metadata.
2+
3+
## Verification Steps
4+
5+
1. Start `msfconsole`
6+
2. Do: `use auxiliary/gather/http_pdf_authors`
7+
3. Do: `set URL [URL]`
8+
4. Do: `run`
9+
10+
11+
## Options
12+
13+
**URL**
14+
15+
The URL of a PDF to analyse.
16+
17+
**URL_LIST**
18+
19+
File containing a list of PDF URLs to analyze.
20+
21+
**OUTFILE**
22+
23+
File to store extracted author names.
24+
25+
26+
## Scenarios
27+
28+
### URL
29+
30+
```
31+
msf auxiliary(http_pdf_authors) > set url http://127.0.0.1/test4.pdf
32+
url => http://127.0.0.1/test4.pdf
33+
msf auxiliary(http_pdf_authors) > run
34+
35+
[*] Processing 1 URLs...
36+
[*] Downloading 'http://127.0.0.1/test4.pdf'
37+
[*] HTTP 200 -- Downloaded PDF (38867 bytes)
38+
[+] PDF Author: Administrator
39+
[*] 100.00% done (1/1 files)
40+
41+
[+] Found 1 authors: Administrator
42+
[*] Auxiliary module execution completed
43+
```
44+
45+
### URL_LIST with OUTFILE
46+
47+
```
48+
msf auxiliary(http_pdf_authors) > set outfile /root/output
49+
outfile => /root/output
50+
msf auxiliary(http_pdf_authors) > set url_list /root/urls
51+
url_list => /root/urls
52+
msf auxiliary(http_pdf_authors) > run
53+
54+
[*] Processing 8 URLs...
55+
[*] Downloading 'http://127.0.0.1:80/test.pdf'
56+
[*] HTTP 200 -- Downloaded PDF (89283 bytes)
57+
[*] 12.50% done (1/8 files)
58+
[*] Downloading 'http://127.0.0.1/test2.pdf'
59+
[*] HTTP 200 -- Downloaded PDF (636661 bytes)
60+
[+] PDF Author: sqlmap developers
61+
[*] 25.00% done (2/8 files)
62+
[*] Downloading 'http://127.0.0.1/test3.pdf'
63+
[*] HTTP 200 -- Downloaded PDF (167478 bytes)
64+
[+] PDF Author: Evil1
65+
[*] 37.50% done (3/8 files)
66+
[*] Downloading 'http://127.0.0.1/test4.pdf'
67+
[*] HTTP 200 -- Downloaded PDF (38867 bytes)
68+
[+] PDF Author: Administrator
69+
[*] 50.00% done (4/8 files)
70+
[*] Downloading 'http://127.0.0.1/test5.pdf'
71+
[*] HTTP 200 -- Downloaded PDF (34312 bytes)
72+
[+] PDF Author: ekama
73+
[*] 62.50% done (5/8 files)
74+
[*] Downloading 'http://127.0.0.1/doesnotexist.pdf'
75+
[*] HTTP 404 -- Downloaded PDF (289 bytes)
76+
[-] Could not parse PDF: PDF is malformed
77+
[*] 75.00% done (6/8 files)
78+
[*] Downloading 'https://127.0.0.1/test.pdf'
79+
[-] Connection failed: Failed to open TCP connection to 127.0.0.1:443 (Connection refused - connect(2) for "127.0.0.1" port 443)
80+
[*] Downloading 'https://127.0.0.1:80/test.pdf'
81+
[-] Connection failed: SSL_connect returned=1 errno=0 state=unknown state: unknown protocol
82+
83+
[+] Found 4 authors: sqlmap developers, Evil1, Administrator, ekama
84+
[*] Writing data to /root/output...
85+
[*] Auxiliary module execution completed
86+
```
87+

metasploit-framework.gemspec

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ Gem::Specification.new do |spec|
111111
spec.add_runtime_dependency 'xmlrpc'
112112
end
113113

114+
#
115+
# File Parsing Libraries
116+
#
117+
# Needed by auxiliary/gather/http_pdf_authors module
118+
spec.add_runtime_dependency 'pdf-reader'
119+
114120
#
115121
# Protocol Libraries
116122
#
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'pdf-reader'
7+
8+
class MetasploitModule < Msf::Auxiliary
9+
10+
def initialize(info = {})
11+
super(update_info(info,
12+
'Name' => 'Gather PDF Authors',
13+
'Description' => %q{
14+
This module downloads PDF files and extracts the author's
15+
name from the document metadata.
16+
},
17+
'License' => MSF_LICENSE,
18+
'Author' => 'Brendan Coles <bcoles[at]gmail.com>'))
19+
register_options(
20+
[
21+
OptString.new('URL', [ false, 'The URL of a PDF to analyse', '' ]),
22+
OptString.new('URL_LIST', [ false, 'File containing a list of PDF URLs to analyze', '' ]),
23+
OptString.new('OUTFILE', [ false, 'File to store output', '' ])
24+
])
25+
register_advanced_options(
26+
[
27+
OptString.new('SSL_VERIFY', [ true, 'Verify SSL certificate', true ]),
28+
OptString.new('PROXY', [ false, 'Proxy server to route connection. <host>:<port>', nil ]),
29+
OptString.new('PROXY_USER', [ false, 'Proxy Server User', nil ]),
30+
OptString.new('PROXY_PASS', [ false, 'Proxy Server Password', nil ])
31+
])
32+
end
33+
34+
def progress(current, total)
35+
done = (current.to_f / total.to_f) * 100
36+
percent = "%3.2f%%" % done.to_f
37+
print_status "%7s done (%d/%d files)" % [percent, current, total]
38+
end
39+
40+
def load_urls
41+
return [ datastore['URL'] ] unless datastore['URL'].to_s.eql? ''
42+
43+
if datastore['URL_LIST'].to_s.eql? ''
44+
fail_with Failure::BadConfig, 'No URL(s) specified'
45+
end
46+
47+
unless File.file? datastore['URL_LIST'].to_s
48+
fail_with Failure::BadConfig, "File '#{datastore['URL_LIST']}' does not exit"
49+
end
50+
51+
File.open(datastore['URL_LIST'], 'rb') {|f| f.read}.split(/\r?\n/)
52+
end
53+
54+
def read(data)
55+
begin
56+
reader = PDF::Reader.new data
57+
return parse reader
58+
rescue PDF::Reader::MalformedPDFError
59+
print_error "Could not parse PDF: PDF is malformed"
60+
return
61+
rescue PDF::Reader::UnsupportedFeatureError
62+
print_error "Could not parse PDF: PDF::Reader::UnsupportedFeatureError"
63+
return
64+
rescue => e
65+
print_error "Could not parse PDF: Unhandled exception: #{e}"
66+
return
67+
end
68+
end
69+
70+
def parse(reader)
71+
# PDF
72+
#print_status "PDF Version: #{reader.pdf_version}"
73+
#print_status "PDF Title: #{reader.info['title']}"
74+
#print_status "PDF Info: #{reader.info}"
75+
#print_status "PDF Metadata: #{reader.metadata}"
76+
#print_status "PDF Pages: #{reader.page_count}"
77+
78+
# Software
79+
#print_status "PDF Creator: #{reader.info[:Creator]}"
80+
#print_status "PDF Producer: #{reader.info[:Producer]}"
81+
82+
# Author
83+
reader.info[:Author].class == String ? reader.info[:Author].split(/\r?\n/).first : ''
84+
end
85+
86+
def download(url)
87+
print_status "Downloading '#{url}'"
88+
89+
begin
90+
target = URI.parse url
91+
raise 'Invalid URL' unless target.scheme =~ %r{https?}
92+
raise 'Invalid URL' if target.host.to_s.eql? ''
93+
rescue => e
94+
print_error "Could not parse URL: #{e}"
95+
return
96+
end
97+
98+
clnt = Net::HTTP::Proxy(@proxysrv, @proxyport, @proxyuser, @proxypass).new(target.host, target.port)
99+
100+
if target.scheme.eql? 'https'
101+
clnt.use_ssl = true
102+
clnt.verify_mode = datastore['SSL_VERIFY'] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
103+
end
104+
105+
headers = {
106+
'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/525.13'
107+
}
108+
109+
begin
110+
res = clnt.get2 target.request_uri, headers
111+
rescue => e
112+
print_error "Connection failed: #{e}"
113+
return
114+
end
115+
116+
unless res
117+
print_error 'Connection failed'
118+
return
119+
end
120+
121+
print_status "HTTP #{res.code} -- Downloaded PDF (#{res.body.length} bytes)"
122+
123+
contents = StringIO.new
124+
contents.puts res.body
125+
contents
126+
end
127+
128+
def write_output(data)
129+
return if datastore['OUTFILE'].to_s.eql? ''
130+
131+
print_status "Writing data to #{datastore['OUTFILE']}..."
132+
file_name = datastore['OUTFILE']
133+
134+
if FileTest::exist?(file_name)
135+
print_status 'OUTFILE already exists, appending..'
136+
end
137+
138+
File.open(file_name, 'ab') do |fd|
139+
fd.write(data)
140+
end
141+
end
142+
143+
def run
144+
if datastore['PROXY']
145+
@proxysrv, @proxyport = datastore['PROXY'].split(':')
146+
@proxyuser = datastore['PROXY_USER']
147+
@proxypass = datastore['PROXY_PASS']
148+
else
149+
@proxysrv, @proxyport = nil, nil
150+
end
151+
152+
urls = load_urls
153+
print_status "Processing #{urls.size} URLs..."
154+
authors = []
155+
max_len = 256
156+
urls.each_with_index do |url, index|
157+
next if url.blank?
158+
contents = download url
159+
next if contents.blank?
160+
author = read contents
161+
unless author.blank?
162+
print_good "PDF Author: #{author}"
163+
if author.length > max_len
164+
print_warning "Warning: Truncated author's name at #{max_len} characters"
165+
authors << author[0...max_len]
166+
else
167+
authors << author
168+
end
169+
end
170+
progress(index + 1, urls.size)
171+
end
172+
173+
print_line
174+
175+
if authors.empty?
176+
print_status 'Found no authors'
177+
return
178+
end
179+
180+
print_good "Found #{authors.size} authors: #{authors.join ', '}"
181+
write_output authors.join "\n"
182+
end
183+
end

0 commit comments

Comments
 (0)