20260930 ‐ poking around on an HP LaserJet 4001dn - rundas-r00t/blog GitHub Wiki
introduction
it's been a while since i've made a blog post. been super busy studying and passing GXPN, working on HackTheBox Academy lessons, competing in a couple CtF's, and working my new job (just completed my first year of professional cybersec work hooray!). recently, i was trying to hack an HP LaserJet Pro 4001dn, which is vulnerable to CVE-2025-26506/7/8 if old firmware is installed, but specifically trying to fuzz CVE-2025-26506 to get the printer to crash, and then, once the buffer size is confirmed, attempt some shellcode. Unfortunately, i was unsuccessful. Just as a side-project, i further experimented with PRET (Printer Exploitation Toolkit) against the HP printer. it's currently coded to python2, but i forked the repo and updated a few of the main python files to python3 syntax for easier use. below is a write up of some of my work with PRET and just poking around & seeing what i could do.
Poking an HP LaserJet Pro 4001dn Over USB with PRET
A walkthrough of using the Printer Exploitation Toolkit (PRET) against an HP LaserJet Pro 4001dn over a direct USB connection...no network required.
Setup
Getting PRET
git clone https://github.com/rundas-r00t/PRET
cd pret
PRET targets Python 2. Running it under Python 3.x requires patching several compatibility issues (see below).
USB Device Access
The printer appears as /dev/usb/lp0. By default it's owned by root:lp with mode 660, so you'll need to either add yourself to the lp group or chmod it:
# Permanent fix: add yourself to the lp group --this is what i ended up doing. it just made things easier in the long-run
sudo usermod -aG lp $USER
newgrp lp # apply in current session without logout
# Or one-shot:
sudo chmod a+rw /dev/usb/lp0
To make the permission survive reboots/replugs via udev:
# Get your printer's vendor:product ID
lsusb | grep -i hp
# e.g. Bus 003 Device 006: ID 03f0:0274 HP, Inc HP LaserJet Pro 4001
sudo tee /etc/udev/rules.d/99-hp-printer.rules << 'EOF'
SUBSYSTEM=="usb", ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0274", MODE="0666"
EOF
sudo udevadm control --reload-rules
sudo udevadm trigger
# Unplug and replug the USB cable
Python 3 Compatibility Patches
PRET 0.40 was written for Python 2. These fixes are required before it will work under Python 3. If you cloned my repo (linked above) these should already be good to go, so you can skip this part. if you cloned the main repo from RUB-NDS, you'll need to apply these changes.
helper.py: USB write requires bytes
The USB write path passes a str to os.write(), which requires bytes in Python 3.
Find the line in class conn that calls os.write(self._file, data) and add a guard before it:
if not isinstance(data, bytes):
data = data.encode()
return os.write(self._file, data)
helper.py: decode binary responses safely
The USB read path calls .decode() with no error handling. Printers return binary data that breaks UTF-8 decoding:
# Before
return os.read(self._file, bytes).decode()
# After
return os.read(self._file, bytes).decode('latin-1')
Invalid regex escape sequences
Python 3.12+ turns invalid escape sequences into SyntaxWarning; Python 3.14 causes some of the escape sequences to be fatal errors. Prefix all regex strings with r"":
# helper.py
PS_CATCH = r'%%\[ (.*)\]%%'
PS_ERROR = r'%%\[ Error: (.*)\]%%'
PS_FLUSH = r'%%\[ Flushing: (.*)\]%%'
# postscript.py, pjl.py, printer.py, pcl.py
# Change any re.split("\s+", ...) → re.split(r"\s+", ...)
# Change any re.findall("\d+", ...) → re.findall(r"\d+", ...)
# etc.
postscript.py: bytes iteration
Python 3 iterates bytes as ints, not chars. In the put command's byte-processing loop:
# Add before the list comprehension that uses ord():
if isinstance(data, bytes):
data = data.decode('latin-1')
pret.py: ASCII art escape
# Line 55: change \| to \\|
print(" |-||\\/_____\\||-. | |´ ...")
After patching, verify with:
python3 -m py_compile pret.py helper.py postscript.py printer.py pjl.py pcl.py
# Should return no output
Connecting
python3 pret.py /dev/usb/lp0 ps # PostScript mode
python3 pret.py /dev/usb/lp0 pjl # PJL mode
Successful connection:
Connection to /dev/usb/lp0 established
Device: HP LaserJet Pro 4001
PJL Enumeration
Device info
/dev/usb/lp0:/> info id
/dev/usb/lp0:/> info status
/dev/usb/lp0:/> info variables
Filesystem info
/dev/usb/lp0:/> info filesys
Returns volume info including a READ-WRITE DIMM (~47 MB):
[0]
NAME=0:\
LABEL=DIMM
TYPE=VOLUME
SIZE=47185920
FREESPACE=45875200
LOCATION=INTERNAL
ACCESS=READ-WRITE
Directory listing
/dev/usb/lp0:/> ls
/dev/usb/lp0:/> ls 0:\
On this printer, ls and directory operations returned no data over USB. PJL filesystem operations (FSDIRLIST, FSQUERY, FSUPLOAD, FSDOWNLOAD) appear to be routed through the network interface only: they accept commands but return empty responses over USB.
PostScript Enumeration
PRET's dump command sends a custom PS payload that introspects PS dictionaries and returns JSON-formatted results.
Note: Large dictionaries like
systemdictandstatusdictoften time out mid-transfer over USB: the USB bulk transfer splits the response and PRET's JSON parser fails. Target smaller dicts first.
System parameters
/dev/usb/lp0:/> dump currentsystemparams
├─ rw- integer BuildTime 1660195497
├─ rw- boolean ByteOrder true
├─ rw- string ControllerSerialNumber ()
├─ rw- integer CurDisplayList 10000
├─ rw- string CurInputDevice %HybridComm%
├─ rw- string CurOutputDevice %HybridComm%
├─ rw- boolean DoPrintErrors false
├─ rw- boolean DoStartPage true
├─ rw- string EngineSWVersion ????
├─ rw- boolean FactoryDefaults false
├─ rw- integer JobTimeout 0
├─ rw- string LicenseID ##-###-###
├─ rw- integer MaxFontCache 8726910
├─ rw- string PrinterName
├─ rw- integer RamSize 64982000
├─ rw- string RealFormat IEEE
├─ rw- integer StartupMode 1
├─ rw- boolean ValidNV true
└─ rw- integer WaitTimeout 300
Every parameter shows rw-: readable and writable with no authentication.
Notable values:
BuildTime: 1660195497→ firmware built 2022-08-11JobTimeout: 0→ no job timeout enforcedEngineSWVersion: ????→ obfuscated by the printerPrinterName: (empty)→ writable, currently unset
User parameters
/dev/usb/lp0:/> dump currentuserparams
├─ rw- integer JobTimeout 0
├─ rw- integer MaxDictStack 530
├─ rw- integer MaxExecStack 10015
├─ rw- integer MaxLocalVM 2147483647
├─ rw- integer MaxOpStack 130000
├─ rw- integer VMReclaim 0
├─ rw- integer VMThreshold 647935
└─ rw- integer WaitTimeout 300
MaxLocalVM: 2147483647 (2³¹−1) means PS VM memory is effectively unlimited. Combined with VMReclaim: 0 (GC disabled) and JobTimeout: 0, a crafted PS job can exhaust device memory: all writable without auth.
Page device parameters
/dev/usb/lp0:/> dump currentpagedevice
├─ rw- boolean Collate false
├─ rw- boolean Duplex false
├─ rw- boolean ManualFeed false
├─ rw- integer ManualFeedTimeout 60
├─ rw- name ProcessColorModel DeviceGray
├─ rw- boolean OutputFaceUp false
├─ rw- boolean PrintOnBackside false
├─ rw- integer PixelDepth 8
└─ rw- boolean UseCIEColor false
statusdict (partial: truncated by USB)
/dev/usb/lp0:/> dump statusdict
Despite truncation, the partial response reveals several operators with rwx permissions (executable, no auth):
├─ r-- string product HP LaserJet Pro 4001
├─ rwx operator checkpassword
├─ rwx operator printer_reset
├─ rwx operator setprintername
├─ rwx operator setjobtimeout
├─ rwx operator dostartpage
├─ rw- boolean eerom true
└─ rw- name defaultfont Courier
printer_reset, setprintername, and checkpassword are all callable without authentication.
Attempting Filesystem Access
PS file write (blocked by sandbox)
/dev/usb/lp0:/> put /tmp/pret-test.txt
PostScript Error (invalidfileaccess; OffendingCommand: file)
Permission denied.
The PS interpreter's sandbox blocks the file operator for write access. This is good from a defensive perspective, but overall quite frustrating for me, as I could not figure out how to escape this sandbox and gain write permissions.
Raw PJL over USB (read mechanism)
PRET's underlying USB read uses a blocking os.read() in a thread. Using select() directly on /dev/usb/lp0 does not work: the usblp kernel driver doesn't signal readability through select. We need a threading approach. Therefore, let's switch to python:
import os, time, threading
def pjl_query(device, cmd):
buf = b''
stop = threading.Event()
def reader():
nonlocal buf
while not stop.is_set():
try:
chunk = os.read(fd, 512)
if chunk:
buf += chunk
except OSError:
break
fd = os.open(device, os.O_RDWR)
t = threading.Thread(target=reader, daemon=True)
t.start()
os.write(fd, b'\x1b%-12345X@PJL\r\n' + cmd + b'\r\n\x1b%-12345X')
time.sleep(4)
stop.set()
t.join(timeout=2)
os.close(fd)
return buf
Verify PJL is alive with an echo:
result = pjl_query('/dev/usb/lp0', b'@PJL ECHO ALIVE')
print(result)
# b'@PJL ECHO ALIVE\r\n\x0c'
PJL filesystem operations (all return empty over USB)
# Directory listing
pjl_query('/dev/usb/lp0', b'@PJL FSDIRLIST NAME="0:\\" ENTRY=1 COUNT=99')
# b''
# Read a file
pjl_query('/dev/usb/lp0', b'@PJL FSUPLOAD FORMAT:BINARY NAME="0:\\pjlpasswd"')
# b''
# Write a file
content = b'test\n'
cmd = b'@PJL FSDOWNLOAD FORMAT:BINARY SIZE=' + str(len(content)).encode() + b' NAME="0:\\test.txt"\r\n' + content
pjl_query('/dev/usb/lp0', cmd)
# b''
PS filenameforall (no filesystem visibility)
import os, time, threading
fd = os.open('/dev/usb/lp0', os.O_RDWR)
# (use same thread reader as above)
ps = b"""%!PS
(0:\\*.*) {
dup print (\\n) print
} 255 string filenameforall
flush
"""
# Result: b''
Conclusion on filesystem access
All USB-side filesystem vectors were exhausted:
| Method | Result |
|---|---|
| PJL FSDIRLIST | No response |
| PJL FSQUERY | No response |
| PJL FSUPLOAD (read) | No response |
| PJL FSDOWNLOAD (write) | No response |
PS file operator (write) |
invalidfileaccess |
PS filenameforall |
No response |
| PJL ECHO | ✅ Works |
PJL communication works fine over USB for status queries. The printer appears to route all PJL filesystem I/O exclusively through its network interface (TCP/9100). USB gets basic PJL and the full PostScript interpreter, but no filesystem operations return data regardless of approach.
Key Takeaways
-
The PS interpreter is fully accessible over USB with zero authentication. Any user who can write to
/dev/usb/lp0can enumerate all device parameters and call privileged operators. -
Every system parameter is writable without auth.
currentsystemparams,currentuserparams, and most ofcurrentpagedeviceall showrw-. This means JobTimeout, VM limits, printer name, print behavior: all changeable. -
Destructive operators are callable without auth.
printer_reset,setprintername, andcheckpasswordinstatusdictare allrwxwith no access control. -
Filesystem access requires network scope. DIMM is READ-WRITE but unreachable over USB. If you have TCP/9100 access, that's a separate: and likely more fruitful: story.
-
select()doesn't work on/dev/usb/lp0. Use a background thread blocking onos.read()for any raw USB I/O with theusblpdriver.