CCYBERCENTAURI
/ SECURITY ATLAS

/ WRITEUP — 2026.08.24

HTB Paperwork writeup

Paperwork is a Linux machine built around a chain of weaknesses in an archive-printing workflow. The route moves through three trust boundaries: an internet-facing LPD service, a local PJL printer service, and a privileged management daemon that mishandles Unix file descriptors.

This guide follows the original investigation without publishing the target IP, passwords, flags, temporary access material, or a copy-ready exploit payload. Treat it as a map of the intended route.

Scope and attack-path summary

The machine exposes SSH, HTTP, and a custom RFC 1179-compatible print service. The overall path is:

  1. Inspect the website and downloadable print-processor source.
  2. Trace an attacker-controlled LPD job name into a shell command.
  3. Enumerate the initial low-privileged foothold and discover a localhost-only PJL service.
  4. Identify unsafe path translation that permits reads and writes outside the printer directory.
  5. Investigate a root management daemon that sends sensitive open file descriptors over a Unix socket.

The important lesson is how small implementation mistakes become serious when several services trust one another.

1. Network enumeration

Begin with a scoped full TCP scan, followed by focused version detection:

ping -c 2 -W 2 TARGET_IP
nmap -Pn -n --min-rate 1000 -T4 -p- TARGET_IP
nmap -Pn -n -sCV --version-all -p<discovered-ports> TARGET_IP

The results reveal SSH, nginx, and a custom printer service. HTTP redirects to paperwork.htb, so add the hostname to your hosts file or resolve it per request:

curl --resolve paperwork.htb:80:TARGET_IP http://paperwork.htb/

The page identifies an archive queue, references RFC 1179, and offers a processor download. Inspect that supplied source before committing time to broad content discovery:

curl --resolve paperwork.htb:80:TARGET_IP \
  http://paperwork.htb/download/archive \
  -o processor.zip

unzip -l processor.zip
unzip -p processor.zip server.py | less

A restrained web scan does not reveal a more useful route. The processor is the primary clue.

2. Initial access: unsafe LPD job handling

RFC 1179 print jobs include a control file. In the supplied processor, a line beginning with J supplies the job name. The server later places that value into a shell-backed logging command:

for line in decoded_content.split("\n"):
    if line.startswith("J"):
        job_name = line[1:]
        break

subprocess.Popen(
    f"echo 'Archive: {job_name}' >> /tmp/archive.log",
    shell=True,
)

The vulnerability is not specific to printing: untrusted text crosses directly into a shell command. A valid proof requires correctly framed LPD traffic rather than sending plaintext to the port.

Build a minimal test client around this sequence:

connect to the LPD port
select the advertised queue
submit one control file and one small data file
change only the J field
confirm execution with a harmless, observable action

Keep the proof non-destructive. The exact quote-breaking string and callback command are intentionally omitted. Successful execution lands as the printer service account. If the shell is difficult to use, a standard PTY upgrade is sufficient:

python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm

3. From the printer account to the user account

Enumerate processes, network listeners, and Unix sockets from the foothold:

id
ss -lntup
ps auxww
find /run -type s -ls 2>/dev/null

A Python-based printer service is listening only on localhost under another user. It speaks PJL. Start with harmless identification and directory queries:

@PJL INFO ID
@PJL FSQUERY NAME="0:\\"

Reading the service implementation exposes its path translation:

def _translate(self, path):
    clean = path.replace("0:", "").replace("\\", "/").lstrip("/")
    return os.path.normpath(os.path.join(self._root, clean))

normpath() collapses .. components, but it does not guarantee that the result remains beneath self._root. The missing containment check turns PJL filesystem operations into a traversal primitive.

Test the boundary first with a non-sensitive file that you can identify safely. Then compare the behavior of the service’s read and write operations. Both pass through the same translator, so the weakness affects more than directory listings.

The intended route uses the write primitive to obtain temporary access as the service owner. Avoid overwriting existing data: inspect the destination, preserve its original contents and permissions, and restore it during cleanup. The exact file path and temporary access material are omitted.

4. From the user account to root

Run conventional privilege checks, but do not stop there:

find / -perm -4000 -type f 2>/dev/null
getcap -r / 2>/dev/null
find /etc/cron* -maxdepth 2 -type f -ls 2>/dev/null
find /run -type s -ls 2>/dev/null

The standard checks do not expose the intended finish. Focus on three related objects:

  • a root-owned printer-management daemon;
  • a Unix socket accessible to the current user;
  • a printer log writable by that same user.

The daemon opens privileged configuration before accepting clients. When it sees suspicious PJL activity in the log, it packages both the log descriptor and the privileged descriptor into an SCM_RIGHTS message.

That design leaks the authority of an already-open file, even though normal filesystem permissions prevent the user from opening it directly. A small receiver can inspect the ancillary data:

import array
import socket

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/run/paperwork/mgmt.sock")

fds = array.array("i")
message, ancillary, *_ = sock.recvmsg(
    4096,
    socket.CMSG_SPACE(8 * fds.itemsize),
)

for level, kind, data in ancillary:
    if level == socket.SOL_SOCKET and kind == socket.SCM_RIGHTS:
        usable = len(data) - (len(data) % fds.itemsize)
        fds.frombytes(data[:usable])

print(message.decode(errors="replace"))
print("received descriptors:", list(fds))

Identify what each descriptor references rather than guessing. One exposes privileged configuration containing a local credential. The credential itself is not included here. In the authorized lab, it completes the final authentication step.

Dead ends worth noting

  • Plaintext commands sent to the custom LPD port fail because the service expects RFC 1179 framing.
  • Generic web enumeration adds little beyond the processor download.
  • Standard SUID, capability, cron, and sudo checks do not reveal the intended escalation.
  • Looking only for private SSH keys misses the PJL write primitive.

These negatives help narrow the investigation toward source review, local services, and Unix IPC.

Cleanup and remediation

Remove any temporary access you created and restore the original file contents, owner, and permissions. Close test listeners and shell sessions when finished.

For defenders, the fixes are direct:

  1. Remove shell=True; write logs directly or invoke fixed argument arrays.
  2. Resolve PJL paths and reject any result outside the configured root for every filesystem operation.
  3. Disable unnecessary PJL writes and run the service under a dedicated, non-login account.
  4. Never pass secret-bearing descriptors to less-trusted clients.
  5. Separate privileged monitoring from user-controlled trigger data and apply least privilege to sockets and logs.
  6. Rotate any credential exposed through the descriptor leak.

Paperwork is a strong reminder that file descriptors carry authority, path cleanup is not path confinement, and unfamiliar protocols can still contain familiar injection bugs.

Practice only on systems you own or are explicitly authorized to test.