Normal view

There are new articles available, click to refresh the page.
Before yesterdayTyranid's Lair

Writing Windows File System Drivers is Hard.

By: tiraniddo
20 May 2020 at 21:29
A tweet by @jonasLyk reminded me of a bug I found in NTFS a few months back, which I've verified still exists in Windows 10 2004. As far as I can tell it's not directly usable to circumvent security but it feels like a bug which could be used in a chain. NTFS is a good demonstration of how complex writing a FS driver is on Windows, so it's hardly surprising that so many weird edges cases pop up over time.

The issue in this case was related to the default Security Descriptor (SD) assignment when creating a new Directory. If you understand anything about Windows SDs you'll know it's possible to specify the inheritance rules through either the CONTAINER_INHERIT_ACE and/or OBJECT_INHERIT_ACE ACE flags. These flags represent whether the ACE should be inherited from a parent directory if the new entry is either a Directory or a File. Let's look at the code which NTFS uses to assign security to a new file and see if you can spot the bug?

The code uses SeAssignSecurityEx to create the new SD based on the Parent SD and any explicit SD from the caller. For inheritance to work you can't specify an explicit SD, so we can ignore that. Whether SeAssignSecurityEx applies the inheritance rules for a Directory or a File depends on the value of the IsDirectoryObject parameter. This is set to TRUE if the FILE_DIRECTORY_FILE options flag was passed to NtCreateFile. That seems fine, you can't create a Directory if you don't specify the FILE_DIRECTORY_FILE flag, if you don't specify a flag then a File will be created by default.

But wait, that's not true at all. If you specify a name of the form ABC::$INDEX_ALLOCATION then NTFS will create a Directory no matter what flags you specify. Therefore the bug is, if you create a directory using the $INDEX_ALLOCATION trick then the new SD will inherit as if it was a File rather than a Directory. We can verifying this behavior on the command prompt.

C:\> mkdir ABC
C:\> icacls ABC /grant "INTERACTIVE":(CI)(IO)(F)
C:\> icacls ABC /grant "NETWORK":(OI)(IO)(F)

First we create a directory ABC and grant two ACEs, one for the INTERACTIVE group will inherit on a Directory, the other for NETWORK will inherit on a File.

C:\> echo "Hello" > ABC\XYZ::$INDEX_ALLOCATION
Incorrect function.

We then create the sub-directory XYZ using the $INDEX_ALLOCATION trick. We can be sure it worked as CMD prints "Incorrect function" when it tries to write "Hello" to the directory object.

C:\> icacls ABC\XYZ
ABC\XYZ NT AUTHORITY\NETWORK:(I)(F)
        NT AUTHORITY\SYSTEM:(I)(F)
        BUILTIN\Administrators:(I)(F)

Dumping the SD for the XYZ sub-directory we see the ACEs were inherited based on it being a File, rather than a Directory as we can see an ACE for NETWORK rather than for INTERACTIVE. Finally we list ABC to verify it really is a directory.

C:\> dir ABC
 Volume in drive C has no label.
 Volume Serial Number is 9A7B-865C

 Directory of C:\ABC

2020-05-20  19:09    <DIR>          .
2020-05-20  19:09    <DIR>          ..
2020-05-20  19:05    <DIR>          XYZ


Is this useful? Honestly probably not. The only scenario I could imagine it would be is if you can specify a path to a system service which creates a file in a location where inherited File access would grant access and inherited Directory access would not. This would allow you to create a Directory you can control, but it seems a bit of a stretch to be honest. If anyone can think of a good use for this let me or Microsoft know :-)

Still, it's interesting that this is another case where $INDEX_ALLOCATION isn't correctly verified where determining whether an object is a Directory or a File. Another good example was CVE-2018-1036, where you could create a new Directory with only FILE_ADD_FILE permission. Quite why this design decision was made to automatically create a Directory when using the stream type is unclear. I guess we might never know.


Silent Exploit Mitigations for the 1%

By: tiraniddo
22 May 2020 at 23:59
With the accelerated release schedule of Windows 10 it's common for new features to be regularly introduced. This is especially true of features to mitigate some poorly designed APIs or easily misused behavior. The problems with many of these mitigations is they're regularly undocumented or at least not exposed through the common Win32 APIs. This means that while Microsoft can be happy and prevent their own code from being vulnerable they leave third party developers to get fucked.

One example of these silent mitigations are the additional OBJECT_ATTRIBUTE flags OBJ_IGNORE_IMPERSONATED_DEVICEMAP and OBJ_DONT_REPARSE which were finally documented, in part because I said it'd be nice if they did so. Of course, it only took 5 years to document them since they were introduced to fix bugs I reported. I guess that's pretty speedy in Microsoft's world. And of course they only help you if you're using the system call APIs which, let's not forget, are only partially documented.

While digging around in Windows 10 2004 (ugh... really, it's just confusing), and probably reminded by Alex Ionescu at some point, I noticed Microsoft have introduced another mitigation which is only available using an undocumented system call and not via any exposed Win32 API. So I thought, I should document it.

UPDATE (2020-04-23): According to @FireF0X this was backported to all supported OS's. So it's a security fix important enough to backport but not tell anyone about. Fantastic.

The system call in question is NtLoadKey3. According to j00ru's system call table this was introduced in Windows 10 2004, however it's at least in Windows 10 1909 as well. As the name suggests (if you're me at least) this loads a Registry Key Hive to an attachment point. This functionality has been extended over time, originally there was only NtLoadKey, then NtLoadKey2 was introduced in XP I believe to add some flags. Then NtLoadKeyEx was introduced to add things like explicit Trusted Hive support to mitigate cross hive symbolic link attacks (which is all j00ru's and Gynvael fault). And now finally NtLoadKey3. I've no idea why it went to 2 then to Ex then back to 3 maybe it's some new Microsoft counting system. The NtLoadKeyEx is partially exposed through the Win32 APIs RegLoadKey and RegLoadAppKey APIs, although they're only expose a subset of the system call's functionality.

Okay, so what bug class is NtLoadKey3 trying to mitigate? One of the problematic behaviors of loading a full Registry Hive (rather that a Per-User Application Hive) is you need to have SeRestorePrivilege* on the caller's Effective Token. SeRestorePrivilege is only granted to Administrators, so in order to call the API successfully you can't be impersonating a low-privileged user. However, the API can also create files when loading the hive file. This includes the hive file itself as well as the recovery log files.

* Don't pay attention to the documentation for RegLoadKey which claims you also need SeBackupPrivilege. Maybe it was required at some point, but it isn't any more.

When loading a system hive such as HKLM\SOFTWARE this isn't an issue as these hives are stored in an Administrator only location (c:\windows\system32\config if you're curious) but sometimes the hives are loaded from user-accessible locations such as from the user's profile or for Desktop Bridge support. In a user accessible location you can use symbolic link tricks to force the logs file to be written to arbitrary locations, and to make matters worse the Security Descriptor of the primary hive file is copied to the log file so it'll be accessible afterwards. An example of just this bug, in this case in Desktop Bridge, is issue 1492 (and 1554 as they didn't fix it properly (╯°□°)╯︵ ┻━┻).

RegLoadKey3 fixes this by introducing an additional parameter to specify an Access Token which will be impersonated when creating any files. This way the check for SeRestorePrivilege can use the caller's Access Token, but any "dangerous" operation will use the user's Token. Of course they could have probably implemented this by adding a new flag which will check the caller's Primary Token for the privilege like they do for SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege but what do I know...

Used appropriately this should completely mitigate the poor design of the system call. For example the User Profile service now uses NtLoadKey3 when loading the hives from the user's profile. How do you call it yourself? I couldn't find any documentation obviously, and even in the usual locations such as OLE32's private symbols there doesn't seem to be any structure data, so I made best guess with the following:

Notice that the TrustKey and Event handles from NtLoadKeyEx have also been folded up into a list of handle values. Perhaps someone wasn't sure if they ever needed to extend the system call whether to go for NtLoadKey4 or NtLoadKeyExEx so they avoided the decision by making the system call more flexible. Also the final parameter, which is also present in NtLoadKeyEx is seemingly unused, or I'm just incapable of tracking down when it gets referenced. Process Hacker's header files claim it's for an IO_STATUS_BLOCK pointer, but I've seen no evidence that's the case.

It'd be really awesome if in this new, sharing and caring Microsoft that they, well shared and cared more often, especially for features important to securing third party applications. TBH I think they're more focused on bringing Wayland to WSL2 or shoving a new API set down developers' throats than documenting things like this.

OBJ_DONT_REPARSE is (mostly) Useless.

By: tiraniddo
23 May 2020 at 10:21
Continuing a theme from the last blog post, I think it's great that the two additional OBJECT_ATTRIBUTE flags were documented as a way of mitigating symbolic link attacks. While OBJ_IGNORE_IMPERSONATED_DEVICEMAP is pretty useful, the other flag, OBJ_DONT_REPARSE isn't, at least not for protecting file system access.

To quote the documentation, OBJ_DONT_REPARSE does the following:

"If this flag is set, no reparse points will be followed when parsing the name of the associated object. If any reparses are encountered the attempt will fail and return an STATUS_REPARSE_POINT_ENCOUNTERED result. This can be used to determine if there are any reparse points in the object's path, in security scenarios."

This seems pretty categorical, if any reparse point is encountered then the name parsing stops and STATUS_REPARSE_POINT_ENCOUNTERED is returned. Let's try it out in PS and open the notepad executable file.

PS> Get-NtFile \??\c:\windows\notepad.exe -ObjectAttributes DontReparse
Get-NtFile : (0xC000050B) - The object manager encountered a reparse point while retrieving an object.

Well that's not what you might expect, there should be no reparse points to access notepad, so what went wrong? We'll you're assuming that the documentation meant NTFS reparse points, when it really meant all reparse points. The C: drive symbolic link is still a reparse point, just for the Object Manager. Therefore just accessing a drive path using this Object Attribute flag fails. Still this does means that it will also work to protect you from Registry Symbolic Links as well as that also uses a Reparse Point.

I'm assuming this flag wasn't introduced for file access at all, but instead for named kernel objects where encountering a Symbolic Link is usually less of a problem. Unlike OBJ_IGNORE_IMPERSONATED_DEVICEMAP I can't pinpoint a specific vulnerability this flag was associated with, so I can't say for certain why it was introduced. Still, it's slightly annoying especially considering there is an IO Manager specific flag, IO_STOP_ON_SYMLINK which does what you'd want to avoid file system symbolic links but that can only be accessed in kernel mode with IoCreateFileEx.

Not that this flag completely protects against Object Manager redirection attacks. It doesn't prevent abuse of shadow directories for example which can be used to redirect path lookups.

PS> $d = Get-NtDirectory \Device
PS> $x = New-NtDirectory \BaseNamedObjects\ABC -ShadowDirectory $d
PS> $f = Get-NtFile \BaseNamedObjects\ABC\HarddiskVolume3\windows\notepad.exe -ObjectAttributes DontReparse
PS> $f.FullPath
\Device\HarddiskVolume3\Windows\notepad.exe

Oh well...

Generating NDR Type Serializers for C#

By: tiraniddo
1 July 2020 at 21:32
As part of updating NtApiDotNet to v1.1.28 I added support for Kerberos authentication tokens. To support this I needed to write the parsing code for Tickets. The majority of the Kerberos protocol uses ASN.1 encoding, however some Microsoft specific parts such as the Privileged Attribute Certificate (PAC) uses Network Data Representation (NDR). This is due to these parts of the protocol being derived from the older NetLogon protocol which uses MSRPC, which in turn uses NDR.

I needed to implement code to parse the NDR stream and return the structured information. As I already had a class to handle NDR I could manually write the C# parser but that'd take some time and it'd have to be carefully written to handle all use cases. It'd be much easier if I could just use my existing NDR byte code parser to extract the structure information from the KERBEROS DLL. I'd fortunately already written the feature, but it can be non-obvious how to use it. Therefore this blog post gives you an overview of how to extract NDR structure data from existing DLLs and create standalone C# type serializer.

First up, how does KERBEROS parse the NDR structure? It could have manual implementations, but it turns out that one of the lesser known features of the MSRPC runtime on Windows is its ability to generate standalone structure and procedure serializers without needing to use an RPC channel. In the documentation this is referred to as Serialization Services.

To implement a Type Serializer you need to do the following in a C/C++ project. First, add the types to serialize inside an IDL file. For example the following defines a simple type to serialize.

interface TypeEncoders
{
    typedef struct _TEST_TYPE
    {
        [unique, string] wchar_t* Name;
        DWORD Value;
    } TEST_TYPE;
}

You then need to create a separate ACF file with the same name as the IDL file (i.e. if you have TYPES.IDL create a file TYPES.ACF) and add the encode and decode attributes.

interface TypeEncoders
{
    typedef [encode, decode] TEST_TYPE;
}

Compiling the IDL file using MIDL you'll get the client source code (such as TYPES_c.c), and you should find a few functions, the most important being TEST_TYPE_Encode and TEST_TYPE_Decode which serialize (encode) and deserialize (decode) a type from a byte stream. How you use these functions is not really important. We're more interested in understanding how the NDR byte code is configured to perform the serialization so that we can parse it and generate our own serializers. 

If you look at the Decode function when compiled for a X64 target it should look like the following:

void
TEST_TYPE_Decode(
    handle_t _MidlEsHandle,
    TEST_TYPE * _pType)
{
    NdrMesTypeDecode3(
         _MidlEsHandle,
         ( PMIDL_TYPE_PICKLING_INFO  )&__MIDL_TypePicklingInfo,
         &TypeEncoders_ProxyInfo,
         TypePicklingOffsetTable,
         0,
         _pType);
}

The NdrMesTypeDecode3 is an API implemented in the RPC runtime DLL. You might be shocked to hear this, but this function and its corresponding NdrMesTypeEncode3 are not documented in MSDN. However, the SDK headers contain enough information to understand how it works.

The API takes 6 parameters:
  1. The serialization handle, used to maintain state such as the current stream position and can be used multiple times to encode or decode more that one structure in a stream.
  2. The MIDL_TYPE_PICKLING_INFO structure. This structure provides some basic information such as the NDR engine flags.
  3. The MIDL_STUBLESS_PROXY_INFO structure. This contains the format strings and transfer types for both DCE and NDR64 syntax encodings.
  4. A list of type offset arrays, these contains the byte offset into the format string (from the Proxy Info structure) for all type serializers.
  5. The index of the type offset in the 4th parameter.
  6. A pointer to the structure to serialize or deserialize.

Only parameters 2 through 5 are needed to parse the NDR byte code correctly. Note that the NdrMesType*3 APIs are used for dual DCE and NDR64 serializers. If you compile as 32 bit it will instead use NdrMesType*2 APIs which only support DCE. I'll mention what you need to parse the DCE only APIs later, but for now most things you'll want to extract are going to have a 64 bit build which will almost always use NdrMesType*3 even though my tooling only parses the DCE NDR byte code.

To parse the type serializers you need to load the DLL you want to extract from into memory using LoadLibrary (to ensure any relocations are processed) then use either the Get-NdrComplexType PS command or the NdrParser::ReadPicklingComplexType method and pass the addresses of the 4 parameters.

Let's look at an example in KERBEROS.DLL. We'll pick the PAC_DEVICE_INFO structure as it's pretty complex and would require a lot of work to manually write a parser. If you disassemble the PAC_DecodeDeviceInfo function you'll see the call to NdrMesTypeDecode3 as follows (from the DLL in Windows 10 2004 SHA1:173767EDD6027F2E1C2BF5CFB97261D2C6A95969).

mov     [rsp+28h], r14  ; pObject
mov     dword ptr [rsp+20h], 5 ; nTypeIndex
lea     r9, off_1800F3138 ; ArrTypeOffset
lea     r8, stru_1800D5EA0 ; pProxyInfo
lea     rdx, stru_1800DEAF0 ; pPicklingInfo
mov     rcx, [rsp+68h]  ; Handle
call    NdrMesTypeDecode3

From this we can extract the following values:

MIDL_TYPE_PICKLING_INFO = 0x1800DEAF0
MIDL_STUBLESS_PROXY_INFO = 0x1800D5EA0
Type Offset Array = 0x1800F3138
Type Offset Index = 5

These addresses are using the default load address of the library which is unlikely to be the same as where the DLL is loaded in memory. Get-NdrComplexType supports specifying relative addresses from a base module, so subtract the base address of 0x180000000 before using them. The following script will extract the type information.

PS> $lib = Import-Win32Module KERBEROS.DLL
PS> $types = Get-NdrComplexType -PicklingInfo 0xDEAF0 -StublessProxy 0xD5EA0 `
     -OffsetTable 0xF3138 -TypeIndex 5 -Module $lib

As long as there was no error from this command the $types variable will now contain the parsed complex types, in this case there'll be more than one. Now you can format them to a C# source code file to use in your application using Format-RpcComplexType.

PS> Format-RpcComplexType $types -Pointer

This will generate a C# file which looks like this. The code contains Encoder and Decoder classes with static methods for each structure. We also passed the Pointer parameter to Format-RpcComplexType. This is so that the structured are wrapped inside a Unique Pointers. This is the default when using the real RPC runtime, although except for Conformant Structures isn't strictly necessary. If you don't do this then the decode will typically fail, certainly in this case.

You might notice a serious issue with the generated code, there are no proper structure names. This is unavoidable, the MIDL compiler doesn't keep any name information with the NDR byte code, only the structure information. However, the basic Visual Studio refactoring tool can make short work of renaming things if you know what the names are supposed to be. You could also manually rename everything in the parsed structure information before using Format-RpcComplexType.

In this case there is an alternative to all that. We can use the fact that the official MS documentation contains a full IDL for PAC_DEVICE_INFO and its related structures and build our own executable with the NDR byte code to extract. How does this help? If you reference the PAC_DEVICE_INFO structure as part of an RPC interface no only can you avoid having to work out the offsets as Get-RpcServer will automatically find the location you can also use an additional feature to extract the type information from your private symbols to fixup the type information.

Create a C++ project and in an IDL file copy the PAC_DEVICE_INFO structures from the protocol documentation. Then add the following RPC server.

[
    uuid(4870536E-23FA-4CD5-9637-3F1A1699D3DC),
    version(1.0),
]
interface RpcServer
{
    int Test([in] handle_t hBinding, 
             [unique] PPAC_DEVICE_INFO device_info);
}

Add the generated server C code to the project and add the following code somewhere to provide a basic implementation:

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

extern "C" void* __RPC_USER MIDL_user_allocate(size_t size) {
    return new char[size];
}

extern "C" void __RPC_USER MIDL_user_free(void* p) {
    delete[] p;
}

int Test(
    handle_t hBinding,
    PPAC_DEVICE_INFO device_info) {
    printf("Test %p\n", device_info);
    return 0;
}

Now compile the executable as a 64-bit release build if you're using 64-bit PS. The release build ensures there's no weird debug stub in front of your function which could confuse the type information. The implementation of Test needs to be unique, otherwise the linker will fold a duplicate function and the type information will be lost, we just printf a unique string.

Now parse the RPC server using Get-RpcServer and format the complex types.

PS> $rpc = Get-RpcServer RpcServer.exe -ResolveStructureNames
PS> Format-RpcComplexType $rpc.ComplexTypes -Pointer

If everything has worked you'll now find the output to be much more useful. Admittedly I also did a bit of further cleanup in my version in NtApiDotNet as I didn't need the encoders and I added some helper functions.

Before leaving this topic I should point out how to handle called to NdrMesType*2 in case you need to extract data from a library which uses that API. The parameters are slightly different to NdrMesType*3.

void
TEST_TYPE_Decode(
    handle_t _MidlEsHandle,
    TEST_TYPE * _pType)
{
    NdrMesTypeDecode2(
         _MidlEsHandle,
         ( PMIDL_TYPE_PICKLING_INFO  )&__MIDL_TypePicklingInfo,
         &TypeEncoders_StubDesc,
         ( PFORMAT_STRING  )&types__MIDL_TypeFormatString.Format[2],
         _pType);
}
  1. The serialization handle.
  2. The MIDL_TYPE_PICKLING_INFO structure.
  3. The MIDL_STUB_DESC structure. This only contains DCE NDR byte code.
  4. A pointer into the format string for the start of the type.
  5. A pointer to the structure to serialize or deserialize.
Again we can discard the first and last parameters. You can then get the addresses of the middle three and pass them to Get-NdrComplexType.

PS> Get-NdrComplexType -PicklingInfo 0x1234 `
    -StubDesc 0x2345 -TypeFormat 0x3456 -Module $lib

You'll notice that there's a offset in the format string (2 in this case) which you can pass instead of the address in memory. It depends what information your disassembler shows:

PS> Get-NdrComplexType -PicklingInfo 0x1234 `
    -StubDesc 0x2345 -TypeOffset 2 -Module $lib

Hopefully this is useful for implementing these NDR serializers in C#. As they don't rely on any native code (or the RPC runtime) you should be able to use them on other platforms in .NET Core even if you can't use the ALPC RPC code.

Using LsaManageSidNameMapping to add a name to a SID.

By: tiraniddo
24 October 2020 at 23:23

I was digging into exactly how service SIDs are mapped back to a name when I came across the API LsaLookupManageSidNameMapping. Unsurprisingly this API is not officially documented either on MSDN or in the Windows SDK. However, LsaManageSidNameMapping is documented (mostly). Turns out that after a little digging they lead to the same RPC function in LSASS, just through different names:

LsaLookupManageSidNameMapping -> lsass!LsaLookuprManageCache

and

LsaManageSidNameMapping -> lsasrv!LsarManageSidNameMapping

They ultimately both end up in lsasrv!LsarManageSidNameMapping. I've no idea why there's two of them and why one is documented but the other not. *shrug*. Of course even though there's an MSDN entry for the function it doesn't seem to actually be documented in the Ntsecapi.h include file *double shrug*. Best documentation I found was this header file.

This got me wondering if I could map all the AppContainer named capabilities via LSASS so that normal applications would resolve them rather than having to do it myself. This would be easier than modifying the SAM or similar tricks. Sadly while you can add some SID to name mappings this API won't let you do that for capability SIDs as there are the following calling restrictions:

  1. The caller needs SeTcbPrivilege (this is a given with an LSA API).
  2. The SID to map must be in the NT security authority (5) and the domain's first RID must be between 80 and 111 inclusive.
  3. You must register a domain SID's name first to use the SID which includes it.
Basically 2 stops us adding a sub-domain SID for a capability as they use the package security authority (15) and we can't just go straight to added the SID to name as we need to have registered the domain with the API, it's not enough that the domain exists. Maybe there's some other easy way to do it, but this isn't it.

Instead I've just put together a .NET tool to add or remove your own SID to name mappings. It's up on github. The mappings are ephemeral so if you break something rebooting should fix it :-)


Creating your own Virtual Service Accounts

By: tiraniddo
26 October 2020 at 23:54

Following on from the previous blog post, if you can't map arbitrary SIDs to names to make displaying capabilities nicer what is the purpose of LsaManageSidNameMapping? The primary purpose is to facilitate the creation of Virtual Service Accounts

A virtual service account allows you to create an access token where the user SID is a service SID, for example, NT SERVICE\TrustedInstaller. A virtual service account doesn't need to have a password configured which makes them ideal for restricting services rather than having to deal with the default service accounts and using WSH to lock them down or specifying a domain user with password.

To create an access token for a virtual service account you can use LogonUserExEx and specify the undocumented (AFAIK) LOGON32_PROVIDER_VIRTUAL logon provider. You must have SeTcbPrivilege to create the token, and the SID of the account must have its first RID in the range 80 to 111 inclusive. Recall from the previous blog post this is exactly the same range that is covered by LsaManageSidNameMapping.

The LogonUserExEx API only takes strings for the domain and username, you can't specify a SID. Using the LsaManageSidNameMapping function allows you to map a username and domain to a virtual service account SID. LSASS prevents you from using RID 80 (NT SERVICE) and 87 (NT TASK) outside of the SCM or the task scheduler service (see this snippet of reversed LSASS code for how it checks). However everything else in the RID range is fair game.

So let's create out own virtual service account. First you need to add your domain and username using the tool from the previous blog post. All these commands need to be run as a user with SeTcbPrivilege.

SetSidMapping.exe S-1-5-100="AWESOME DOMAIN" 
SetSidMapping.exe S-1-5-100-1="AWESOME DOMAIN\USER"

So we now have the AWESOME DOMAIN\USER account with the SID S-1-5-100-1. Now before we can login the account you need to grant it a logon right. This is normally SeServiceLogonRight if you wanted a service account, but you can specify any logon right you like, even SeInteractiveLogonRight (sadly I don't believe you can actually login with your virtual account, at least easily).

If you get the latest version of NtObjectManager (from github at the time of writing) you can use the Add-NtAccountRight command to add the logon type.

PS> Add-NtAccountRight -Sid 'S-1-5-100-1' -LogonType SeInteractiveLogonRight

Once granted a logon right you can use the Get-NtToken command to logon the account and return a token.

PS> $token = Get-NtToken -Logon -LogonType Interactive -User USER -Domain 'AWESOME DOMAIN' -LogonProvider Virtual
PS> Format-NtToken $token
AWESOME DOMAIN\USER

As you can see we've authenticated the virtual account and got back a token. As we chose to logon as an interactive type the token will also have the INTERACTIVE group assigned. Anyway that's all for now. I guess as there's only a limited number of RIDs available (which is an artificial restriction) MS don't want document these features even though it could be a useful thing for normal developers.



Standard Activating Yourself to Greatness

By: tiraniddo
27 April 2021 at 23:45

This week @decoder_it and @splinter_code disclosed a new way of abusing DCOM/RPC NTLM relay attacks to access remote servers. This relied on the fact that if you're in logged in as a user on session 0 (such as through PowerShell remoting) and you call CoGetInstanceFromIStorage the DCOM activator would create the object on the lowest interactive session rather than the session 0. Once an object is created the initial unmarshal of the IStorage object would happen in the context of the user authenticated to that session. If that happens to be a privileged user such as a Domain Administrator then the NTLM authentication could be relayed to a remote server and fun ensues.

The obvious problem with this attack is the requirement of being in session 0. Certainly it's possible a non-admin user might be allowed to authenticate to a system via PowerShell remoting but it'd be rarer than just being authenticated on a Terminal Server with multiple other users you could attack. It'd be nice if somehow you could pick the session that the object was created on.

Of course this already exists, you can use the session moniker to activate an object cross-session (other than to session 0 which is special). I've abused this feature multiple times for cross-session attacks, such as this, this or this. I've repeated told Microsoft they need to fix this activation route as it makes no sense than a non-administrator can do it. But my warnings have not been heeded. 

If you read the description of the session moniker you might notice a problem for us, it can't be combined with IStorage activation. The COM APIs only give us one or the other. However, if you poke around at the DCOM protocol documentation you'll notice that they are technically independent. The session activation is specified by setting the dwSessionId field in the SpecialPropertiesData activation property. And the marshalled IStorage object can be passed in the ifdStg field of the InstanceInfoData activation property. You package those activation properties up and send them to the IRemoteSCMActivator RemoteGetClassObject or RemoteCreateInstance methods. Of course it's possible this won't really work, but at least they are independent properties and could be mixed.

The problem with testing this out is implementing DCOM activation is ugly. The activation properties first need to be NDR marshalled in a blob. They then need to be packaged up correctly before it can be sent to the activator. Also the documentation is only for remote activation which is not we want, and there are some weird quirks of local activation I'm not going to go into. Is there any documented way to access the activator without doing all this?

No, sorry. There is an undocumented way though if you're interested? Sure? Okay good, let's carry on. The key with these sorts of challenges is to just look at how the system already does it. Specifically we can look at how session moniker is activating the object and maybe from that we'll be lucky and we can reuse that for our own purposes.

Where to start? If you read this MSDN article you can see you need to call MkParseDisplayNameEx to create parse the string into a moniker. But that's really a wrapper over MkParseDisplayName to provide URL moniker functionality which we don't care about. We'll just start at the MkParseDisplayName which is in OLE32.

HRESULT MkParseDisplayName(LPBC pbc, LPCOLESTR szUserName, 
      ULONG *pchEaten, LPMONIKER *ppmk) {
  HRESULT hr = FindLUAMoniker(pbc, szUserName, &pcchEaten, &ppmk);
  if (hr == MK_E_UNAVAILABLE) {
    hr = FindSessionMoniker(pbc, szUserName, &pcchEaten, &ppmk);
  }
  // Parse rest of moniker.
}

Almost immediately we see a call to FindSessionMoniker, seems promising. Looking into that function we find what we need.

HRESULT FindSessionMoniker(LPBC pbc, LPCWSTR pszDisplayName, 
                           ULONG *pchEaten, LPMONIKER *ppmk) {
  DWORD dwSessionId = 0;
  BOOL bConsole = FALSE;
  
  if (wcsnicmp(pszDisplayName, L"Session:", 8))
    return MK_E_UNAVAILABLE;
  
  
if (!wcsnicmp(pszDisplayName + 8, L"Console", 7)) {
    dwConsole = TRUE;
    *pcbEaten = 15;
  } else {
    LPWSTR EndPtr;
    dwSessionId = wcstoul(pszDisplayName + 8, &End, 0);
    *pcbEaten = EndPtr - pszDisplayName;
  }

  *ppmk = new CSessionMoniker(dwSessionId, bConsole);
  return S_OK;
}

This code parses out the session moniker data and then creates a new instance of the CSessionMoniker class. Of course this is not doing any activation yet. You don't use the session moniker in isolation, instead you're supposed to build a composite moniker with a new or class moniker. The MkParseDisplayName API will keep parsing the string (which is why pchEaten is updated) and combine each moniker it finds. Therefore, if you have the moniker display name:

Session:3!clsid:0002DF02-0000-0000-C000-000000000046

The API will return a composite moniker consisting of the session moniker for session 3 and the class moniker for CLSID 0002DF02-0000-0000-C000-000000000046 which is the Browser Broker. The example code then calls BindToObject on the composite moniker, which first calls the right most moniker, which is the class moniker.

HRESULT CClassMoniker::BindToObject(LPBC pbc, 
  LPMONIKER pmkToLeft, REFIID riid, void **ppv) {
  if (pmkToLeft) {
      IClassActivator pClassActivator;
      pmkToLeft->BindToObject(pcb, nullptr, 
        IID_IClassActivator, &pClassActivator);
      return pClassActivator->GetClassObject(m_clsid, 
            CLSCTX_SERVER, 0, riid, ppv);

  }
  // ...
}

The pmkToLeft parameter is set by the composite moniker to the left moniker, which is the session moniker. We can see that the class moniker calls the session moniker's BindToObject method requesting an IClassActivator interface. It then calls the GetClassObject method, passing it the CLSID to activate. We're almost there.

HRESULT CSessionMoniker::GetClassObject(
   REFCLSID pClassID, CLSCTX dwClsContext, 
   LCID locale, REFIID riid, void **ppv) {
  IStandardActivator* pActivator;
  CoCreateInstance(&CLSID_ComActivator, NULL, CLSCTX_INPROC_SERVER, 
    IID_IStandardActivator, &pActivator);

 
  ISpecialSystemProperties pSpecialProperties;
  pActivator->QueryInterface(IID_ISpecialSystemProperties, 
      &pSpecialProperties);
  pSpecialProperties->SetSessionId(m_sessionid, m_console, TRUE);
  return pActivator->StandardGetClassObject(pClassId, dwClsContext, 
                                            NULL, riid, ppv);

}

Finally the session moniker creates a new COM activator object with the IStandardActivator interface. It then queries for the ISpecialSystemProperties interface and sets the moniker's session ID and console state. It then calls the StandardGetClassObject method on the IStandardActivator and you should now have a COM server cross-session. None of these interface or the class are officially documented of course (AFAIK).

The $1000 question is, can you also do IStorage activation through the IStandardActivator interface? Poking around in COMBASE for the implementation of the interface you find one of its functions is:

HRESULT StandardGetInstanceFromIStorage(COSERVERINFO* pServerInfo, 
  REFCLSID pclsidOverride, IUnknown* punkOuter, CLSCTX dwClsCtx, 
  IStorage* pstg, int dwCount, MULTI_QI pResults[]);

It seems that the answer is yes. Of course it's possible that you still can't mix the two things up. That's why I wrote a quick and dirty example in C#, which is available here. Seems to work fine. Of course I've not tested it out with the actual vulnerability to see it works in that scenario. That's something for others to do.

Dumping Stored Credentials with SeTrustedCredmanAccessPrivilege

By: tiraniddo
21 May 2021 at 07:03

I've been going through the various token privileges on Windows trying to find where they're used. One which looked interesting is SeTrustedCredmanAccessPrivilege which is documented as "Access Credential Manager as a trusted caller". The Credential Manager allows a user to store credentials, such as web or domain accounts in a central location that only they can access. It's protected using DPAPI so in theory it's only accessible when the user has authenticated to the system. The question is, what does having SeTrustedCredmanAccessPrivilege grant? I couldn't immediately find anyone who'd bothered to document it, so I guess I'll have to do it myself.

The Credential Manager is one of those features that probably sounded great in the design stage, but does introduce security risks, especially if it's used to store privileged domain credentials, such as for remote desktop access. An application, such as the remote desktop client, can store domain credential using the CredWrite API and specifying the username and password in the CREDENTIAL structure. The type of credentials should be set to CRED_TYPE_DOMAIN_PASSWORD.

An application can then access the stored credentials for the current user using APIs such as CredRead or CredEnumerate. However, if the type of credential is CRED_TYPE_DOMAIN_PASSWORD the CredentialBlob field which should contain the password is always empty. This is an artificial restriction put in place by LSASS which implements the credential manager RPC service. If a domain credentials type is being read then it will never return the password.

How does the domain credentials get used if you can't read the password? Security packages such as NTLM/Kerberos/TSSSP which are running within the LSASS process can use an internal API which doesn't restrict the reading of the domain password. Therefore, when you authenticate to the remote desktop service the target name is used to lookup available credentials, if they exist the user will be automatically authenticated.

The credentials are stored in files in the user's profile encrypted with the user's DPAPI key. Why can we not just decrypt the file directly to get the password? When writing the file LSASS sets a system flag in the encrypted blob which makes the DPAPI refuse to decrypt the blob even though it's still under a user's key. Only code running in LSASS can call the DPAPI to decrypt the blob.

If we have administrator privileges getting access the password is trivial. Read the Mimikatz wiki page to understand the various ways that you can use the tool to get access to the credentials. However, it boils down to one of the following approaches:

  1. Patch out the checks in LSASS to not blank the password when read from a normal user.
  2. Inject code into LSASS to decrypt the file or read the credentials.
  3. Just read them from LSASS's memory.
  4. Reimplement DPAPI with knowledge of the user's password to ignore the system flag.
  5. Play games with the domain key backup protocol.
For example, Nirsoft's CredentialsFileView seems to use the injection into LSASS technique to decrypt the DPAPI protected credential files. (Caveat, I've only looked at v1.07 as v1.10 seems to not be available for download anymore, so maybe it's now different. UPDATE: it seems available for download again but Defender thinks it's malware, plus ça change).

At this point you can probably guess that SeTrustedCredmanAccessPrivilege allows a caller to get access to a user's credentials. But how exactly? Looking at LSASRV.DLL which contains the implementation of the Credential Manager the privilege is checked in the function CredpIsRpcClientTrusted. This is only called by two APIs, CredrReadByTokenHandle and CredrBackupCredentials which are exported through the CredReadByTokenHandle and CredBackupCredentials APIs.

The CredReadByTokenHandle API isn't that interesting, it's basically CredRead but allows the user to read from to be specified by providing the user's token. As far as I can tell reading a domain credential still returns a blank password. CredBackupCredentials on the other hand is interesting. It's the API used by CREDWIZ.EXE to backup a user's credentials, which can then be restored at a later time. This backup includes all credentials including domain credentials. The prototype for the API is as follows:

BOOL WINAPI CredBackupCredentials(HANDLE Token, 
                                  LPCWSTR Path, 
                                  PVOID Password, 
                                  DWORD PasswordSize, 
                                  DWORD Flags);

The backup process is slightly convoluted, first you run CREDWIZ on your desktop and select backup and specify the file you want to write the backup to. When you continue with the backup the process makes an RPC call to your WinLogon process with the credentials path which spawns a new copy of CREDWIZ on the secure desktop. At this point you're instructed to use CTRL+ALT+DEL to switch to the secure desktop. Here you type the password, which is used to encrypt the file to protect it at rest, and is needed when the credentials are restored. CREDWIZ will even ensure it meets your system's password policy for complexity, how generous.

CREDWIZ first stores the file to a temporary file, as LSASS encrypts the encrypted contents with the system DPAPI key. The file can be decrypted then written to the final destination, with appropriate impersonation etc.

The only requirement for calling this API is having the SeTrustedCredmanAccessPrivilege privilege enabled. Assuming we're an administrator getting this privilege is easy as we can just borrow a token from another process. For example, checking for what processes have the privilege shows obviously WinLogon but also LSASS itself even though it arguably doesn't need it.

PS> $ts = Get-AccessibleToken
PS> $ts | ? { 
   "SeTrustedCredmanAccessPrivilege" -in $_.ProcessTokenInfo.Privileges.Name 
}
TokenId Access                                  Name
------- ------                                  ----
1A41253 GenericExecute|GenericRead    LsaIso.exe:124
1A41253 GenericExecute|GenericRead     lsass.exe:672
1A41253 GenericExecute|GenericRead winlogon.exe:1052
1A41253 GenericExecute|GenericRead atieclxx.exe:4364

I've literally no idea what the ATIECLXX.EXE process is doing with SeTrustedCredmanAccessPrivilege, it's probably best not to ask ;-)

To use this API to backup a user's credentials as an administrator you do the following. 
  1. Open a WinLogon process for PROCESS_QUERY_LIMITED_INFORMATION access and get a handle to its token with TOKEN_DUPLICATE access.
  2. Duplicate token into an impersonation token, then enable SeTrustedCredmanAccessPrivilege.
  3. Open a token to the target user, who must already be authenticated.
  4. Call CredBackupCredentials while impersonating the WinLogon token passing a path to write to and a NULL password to disable the user encryption (just to make life easier). It's CREDWIZ which enforces the password policy not the API.
  5. While still impersonating open the file and decrypt it using the CryptUnprotectData API, write back out the decrypted data.
If it all goes well you'll have all the of the user's credentials in a packed binary format. I couldn't immediately find anyone documenting it, but people obviously have done before. I'll leave doing all this yourself as a exercise for the reader. I don't feel like providing an implementation.


Why would you do this when there already exists plenty of other options? The main advantage, if you can call it that, it you never touch LSASS and definitely never inject any code into it. This wouldn't be possible anyway if LSASS is running as PPL. You also don't need to access the SECURITY hive to extract DPAPI credentials or know the user's password (assuming they're authenticated of course). About the only slightly suspicious thing is opening WinLogon to get a token, though there might be alternative approaches to get a suitable token.



The Much Misunderstood SeRelabelPrivilege

By: tiraniddo
2 June 2021 at 21:49

Based on my previous blog post I recently had a conversation with a friend and well-known Windows security researcher about token privileges. Specifically, I was musing on how SeTrustedCredmanAccessPrivilege is not a "God" privilege. After some back and forth it seemed we were talking at cross purposes. My concept of a "God" privilege is one which the kernel considers to make a token elevated (see Reading Your Way Around UAC (Part 3)) and so doesn't make it available to any token with an integrity level less than High. They on the other hand consider such a privilege to be one where you can directly compromise a resource or the OS as a whole by having the privilege enabled, this might include privileges which aren't strictly a "God" from the kernel's perspective but can still allow system compromise.

After realizing the misunderstanding I was still surprised that one of the privileges in their list wasn't considering a "God", specifically SeRelabelPrivilege. It seems that there's perhaps some confusion as to what this privilege actually allows you to do, so I thought it'd be worth clearing it up.

Point of pedantry: I don't believe it's correct to say that a resource has an integrity level. It instead has a mandatory label, which is stored in an ACE in the SACL. That ACE contains a SID which maps to an integrity level and an mandatory policy which is stored in the access mask. The combination of integrity level and policy is what determines what access is granted (although you can't grant write up through the policy). The token on the other hand does have an integrity level and a separate mandatory policy, which isn't the same as the one in the ACE. Oddly you specify the value when calling SetTokenInformation using a TOKEN_MANDATORY_LABEL structure, confusing I know.

As with a lot of privileges which don't get used very often the official documentation is not great. You can find the MSDN documentation here. The page is worse than usual as it seems to have been written at a time in the Vista/Longhorn development when the Mandatory Integrity Control (MIC) (or as it calls it Windows Integrity Control (WIC)) feature was still in flux. For example, it mentions an integrity level above System, called Installer. Presumably Installer was the initial idea to block administrators modifying system files, which was replaced by the TrustedInstaller SID as the owner (see previous blog posts). There is a level above System in Vista, called Protected Process, which is not usable as protected processes was implementing using a different mechanism. 

Distilling what the documentation says the privilege does, it allows for two operations. First it allows you to set the integrity level in a mandatory label ACE to be above the caller's token integrity level. Normally as long as you've been granted WRITE_OWNER access to a resource you can set the label's integrity level to any value less than or equal to the caller's integrity level.

For example, if you try to set the resource's label to System, but the caller is only at High then the operation fails with the STATUS_INVALID_LABEL error. If you enable SeRelabelPrivilege then you can set this operation will succeed. 

Note, the privilege doesn't allow you to raise the integrity level of a token, you need SeTcbPrivilege for that. You can't even raise the integrity level to be less than or equal to the caller's integrity level, the operation can only decrease the level in the token without SeTcbPrivilege.

The second operation is that you can decrease the label. In general you can always decrease the label without the privilege, unless the resource's label is above the callers. For example you can set the label to Low without any special privilege, as long as you have WRITE_OWNER access on the handle and the current label is less than or equal to the caller's. However, if the label is System and the caller is High then they can't decrease the label and the privilege is required.

The documentation has this to say (emphasis mine):

"If malicious software is set with an elevated integrity level such as Trusted Installer or System, administrator accounts do not have sufficient integrity levels to delete the program from the system. In that case, use of the Modify an object label right is mandated so that the object can be relabeled. However, the relabeling must occur by using a process that is at the same or a higher level of integrity than the object that you are attempting to relabel."

This is a very confused paragraph. First it indicates that an administrator can't delete resource with Trusted Installer or System integrity labels and so requires the privilege to relabel. And then it says that the process doing the relabeling must be at a greater or equal integrity level to do the relabeling. Which if that is the case you don't need the privilege. Perhaps the original design on mandatory labels was more sticky, as in maybe you always needed SeRelabelPrivilege to reduce the label regardless of its current value?

At any rate the only user that gets SeRelabelPrivilege by default is SYSTEM, which defaults to the System integrity level which is already the maximum allowed level so this behavior of the privilege seems pretty much moot. At any rate as it's a "God" privilege it will be disabled if the token has an integrity level less than High, so this lowering operation is going to be rarely useful.

This leads in to the most misunderstood part which if you squint you might be able to grasp from the privilege's documentation. The ability to lower the label of a resource is mostly dependent on whether the caller can get WRITE_OWNER access to the resource. However, the WRITE_OWNER access right is typically part of GENERIC_ALL in the generic mapping, which means it will never be granted to a caller with a lower integrity level regardless of the DACL or whether they're the owner. 

This is the interesting thing the privilege brings to the lowering operation, it allows the caller to circumvent the MIC check for WRITE_OWNER. This then allows the caller to open for WRITE_OWNER a higher labeled resource and then change the label to any level it likes. This works the same way as SeTakeOwnershipPrivilege, in that it grants WRITE_OWNER without ever checking the DACL. However, if you use SeTakeOwnershipPrivilege it'll still be subject to the MIC check and will not grant access if the label is above the caller's integrity level.

The problem with this privilege is down to the design of MIC, specifically that WRITE_OWNER is overloaded to allow setting the resource's mandatory label but also its traditional use of setting the owner. There's no way for the kernel to distinguish between the two operations once the access has been granted (or at least it doesn't try to distinguish). 

Surely, there is some limitation on what type of resource can be granted WRITE_OWNER access? Nope, it seems that even if the caller does not have any access rights to the resource it will still be granted WRITE_OWNER access. This makes the SeRelabelPrivilege exactly like SeTakeOwnershipPrivilege but with the adding feature of circumventing the MIC check. Summarizing, a token with SeRelabelPrivilege enabled can take ownership of any resource it likes, even one which has a higher label than the caller.

You can of course verify this yourself, here's some PowerShell script using NtObjectManager which you should run as an administrator. The script creates a security descriptor which doesn't grant SYSTEM any access, then tries to request WRITE_OWNER without and with SeRelabelPrivilege.

PS> $sd = New-NtSecurityDescriptor "O:ANG:AND:(A;;GA;;;AN)" -Type Directory
PS> Invoke-NtToken -System {
   Get-NtGrantedAccess -SecurityDescriptor $sd -Access WriteOwner -PassResult
}
Status               Granted Access Privileges
------               -------------- ----------
STATUS_ACCESS_DENIED 0              NONE

PS> Invoke-NtToken -System {
   Enable-NtTokenPrivilege SeRelabelPrivilege
   Get-NtGrantedAccess -SecurityDescriptor $sd -Access WriteOwner -PassResult
}
Status         Granted Access Privileges
------         -------------- ----------
STATUS_SUCCESS WriteOwner     SeRelabelPrivilege

The fact that this behavior is never made explicit is probably why my friend didn't realize its behavior before. This coupled with the privilege's rare usage, only being granted by default to SYSTEM means it's not really a problem in any meaningful sense. It would be interesting to know the design choices which led to the privilege being created, it seems like its role was significantly more important at some point and became almost vestigial during the Vista development process. 

If you've read this far is there any actual useful scenario for this privilege? The only resources which typically have elevated labels are processes and threads. You can already circumvent the MIC check using SeDebugPrivilege. Of course usage of that privilege is probably watched like a hawk, so you could abuse this privilege to get full access to an elevated process, by accessing changing the owner to the caller and lowering the label. Once you're the owner with a low label you can then modify the DACL to grant full access directly without SeDebugPrivilege.

However, as only SYSTEM gets the privilege by default you'd need to impersonate the token, which would probably just allow you to access the process anyway. So mostly it's mostly a useless quirk unless the system you're looking at has granted it to the service accounts which might then open the door slightly to escaping to SYSTEM.

A Little More on the Task Scheduler's Service Account Usage

By: tiraniddo
12 June 2021 at 05:42

Recently I was playing around with a service which was running under a full virtual service account rather than LOCAL SERVICE or NETWORK SERVICE, but it had SeImpersonatePrivilege removed. Looking for a solution I recalled that Andrea Pierini had posted a blog about using virtual service accounts, so I thought I'd look there for inspiration. One thing which was interesting is that he mentioned that a technique abusing the task scheduler found by Clément Labro, which worked for LS or NS, didn't work when using virtual service accounts. I thought I should investigate it further, out of curiosity, and in the process I found an sneaky technique you can use for other purposes.

I've already blogged about the task scheduler's use of service accounts. Specifically in a previous blog post I discussed how you could get the TrustedInstaller group by running a scheduled task using the service SID. As the service SID is the same name as used when you are using a virtual service account it's clear that the problem lies in the way in this functionality is implemented and that it's likely distinct from how LS or NS token's are created.

The core process creation code for the task scheduler in Windows 10 is actually in the Unified Background Process Manager (UBPM) DLL, rather than in the task scheduler itself. A quick look at that DLL we find the following code:

HANDLE UbpmpTokenGetNonInteractiveToken(PSID PrincipalSid) {

  // ...

  if (UbpmUtilsIsServiceSid(PrinicpalSid)) {

    return UbpmpTokenGetServiceAccountToken(PrinicpalSid);

  }

  if (EqualSid(PrinicpalSid, kNetworkService)) {

    Domain = L"NT AUTHORITY";

    User = L"NetworkService";

  } else if (EqualSid(PrinicpalSid, kLocalService)) {

    Domain = L"NT AUTHORITY";

    User = L"LocalService";

  }

  HANDLE Token;

  if (LogonUserExExW(User, Domain, Password, 

    LOGON32_LOGON_SERVICE, 

    LOGON32_PROVIDER_DEFAULT, &Token)) {

    return Token;

  }

  // ...

}


This UbpmpTokenGetNonInteractiveToken function is taking the principal SID from the task registration or passed to RunEx and determining what it represents to get back the token. It checks if the SID is a service SID, by which is means the NT SERVICE\NAME SID we used in the previous blog post. If it is it calls a separate function, UbpmpTokenGetServiceAccountToken to get the service token.

Otherwise if the SID is NS or LS then it specifies the well know names for those SIDs and called LogonUserExEx with the LOGON32_LOGON_SERVICE type. The UbpmpTokenGetServiceAccountToken function does the following:

TOKEN UbpmpTokenGetServiceAccountToken(PSID PrincipalSid) {

  LPCWSTR Name = UbpmUtilsGetAccountNamesFromSid(PrincipalSid);

  SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

  SC_HANDLE service = OpenService(scm, Name, SERVICE_ALL_ACCESS);

  HANDLE Token;

  GetServiceProcessToken(g_ScheduleServiceHandle, service, &Token);

  return Token;

}

This function gets the name from the service SID, which is the name of the service itself and opens it for all access rights (SERVICE_ALL_ACCESS). If that succeeds then it passes the service handle to an undocumented SCM API, GetServiceProcessToken, which returns the token for the service. Looking at the implementation in SCM this basically uses the exact same code as it would use for creating the token for starting the service. 

This is why there's a distinction between LS/NS and a virtual service account using Clément's technique. If you use LS/NS the task scheduler gets a fresh token from the LSA with no regards to how the service is configured. Therefore the new token has SeImpersonatePrivilege (or what ever else is allowed). However for a virtual service account the service asks the SCM for the service's token, as the SCM knows about what restrictions are in place it honours things like privileges or the SID type. Therefore the returned token will be stripped of SeImpersonatePrivilege again even though it'll technically be a different token to the currently running service.

Why does the task scheduler need some undocumented function to get the service token? As I mentioned in a previous blog post about virtual accounts only the SCM (well technically the first process to claim it's the SCM) is allowed to authenticate a token with a virtual service account. This seems kind of pointless if you ask me as you already need SeTcbPrivilege to create the service token, but it is what it is.

Okay, so now we know why Clément's technique doesn't get you back any privileges. You might now be asking, so what? Well one interesting behavior came from looking at how the task scheduler determines if you're allowed to specify a service SID as a principal. In my blog post of creating a task running as TrustedInstaller I implied it needed administrator access, which is sort of true and sort of not. Let's see the function the task scheduler uses to determine if the caller's allowed to run a task as a specified principal.

BOOL IsPrincipalAllowed(User& principal) {

  RpcAutoImpersonate::RpcAutoImpersonate();

  User caller;

  User::FromImpersonationToken(&caller);

  RpcRevertToSelf();

  if (tsched::IsUserAdmin(caller) || 

      caller.IsLocalSystem(caller)) {

    return TRUE;

  }

  

  if (principal == caller) {

    return TRUE;

  }


  if (principal.IsServiceSid()) {

    LPCWSTR Name = principal.GetAccount();

    RpcAutoImpersonate::RpcAutoImpersonate();

    SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

    SC_HANDLE service = OpenService(scm, Name, SERVICE_ALL_ACCESS);

    RpcRevertToSelf();

    if (service) {

      return TRUE;

    }

  }

  return FALSE;

}

The IsPrincipalAllowed function first checks if the caller is an administrator or SYSTEM. If it is then any principal is allowed (again not completely true, but good enough). Next it checks if the principal's user SID matches the one we're setting. This is what would allow NS/LS or a virtual service account to specify a task running as their own user account. 


Finally, if the principal is a service SID, then it tries to open the service for full access while impersonating the caller. If that succeeds it allows the service SID to be used as a principal. This behaviour is interesting as it allows for a sneaky way to abuse badly configured services. 


It's a well known check for privilege escalation that you enumerate all local services and see if any of them grant a normal user privileged access rights, mainly SERVICE_CHANGE_CONFIG. This is enough to hijack the service and get arbitrary code running as the service account. A common trick is to change the executable path and restart the service, but this isn't great for a few different reasons.

  1. Changing the executable path could easily be noticed.
  2. You probably want to fix the path back again afterwards, which is just a pain.
  3. If the service is currently running you'll need stop the service, then restart the modified service to get the code execution.
However, as long as your account is granted full access to the service you can use the task scheduler even without being an administrator to get code running as the service's user account, such as SYSTEM, without ever needing to modify the service's configuration directly or stop/start the service. Much more sneaky. Of course this does mean that the token the task runs under might have privileges stripped etc, but that's something which is easy enough to deal with (as long as it's not write restricted).

This is a good lesson on how to never take things on face value. I just assumed the caller would need administrator privileges to set the service account as the principal for a task. But it seems that's not actually required if you dig into the code. Hopefully someone will find it useful.

Footnote: If you read this far, you might also ask, can you get back SeImpersonatePrivilege from a virtual service account or not? Of course, you just use the named pipe trick I described in a previous blog post. Because of the way that the token is created the token stored in the logon session will still have all the assigned privileges. You can extract the token by using the named pipe to your own service, and use that to create a new process and get back all the missing privileges.






How to secure a Windows RPC Server, and how not to.

By: tiraniddo
15 August 2021 at 02:04

The PetitPotam technique is still fresh in people's minds. While it's not directly an exploit it's a useful step to get unauthenticated NTLM from a privileged account to forward to something like the AD CS Web Enrollment service to compromise a Windows domain. Interestingly after Microsoft initially shrugged about fixing any of this they went and released a fix, although it seems to be insufficient at the time of writing.

While there's plenty of details about how to abuse the EFSRPC interface, there's little on why it's exploitable to begin with. I thought it'd be good to have a quick overview of how Windows RPC interfaces are secured and then by extension why it's possible to use the EFSRPC interface unauthenticated. 

Caveat: No doubt I might be missing other security checks in RPC, these are the main ones I know about :-)

RPC Server Security

The server security of RPC is one which has seemingly built up over time. Therefore there's various ways of doing it, and some ways are better than others. There are basically three approaches, which can be mixed and matched:
  1. Securing the endpoint
  2. Securing the interface
  3. Ad-hoc security
Let's take each one in turn to determine how each one secures the RPC server.

Securing the Endpoint

You register the endpoint that the RPC server will listen on using the RpcServerUseProtseqEp API. This API takes the type of endpoint, such as ncalrpc (ALPC), ncacn_np (named pipe) or ncacn_ip_tcp (TCP socket) and creates the listening endpoint. For example the following would create a named pipe endpoint called DEMO.

RpcServerUseProtseqEp(
    L"ncacn_np",
    RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
    L"\\pipe\\DEMO",
    nullptr);

The final parameter is optional but represents a security descriptor (SD) you assign to the endpoint to limit who has access. This can only be enforced on ALPC and named pipes as something like a TCP socket doesn't (technically) have an access check when it's connected to. If you don't specify an SD then a default is assigned. For a named pipe the default DACL grants the following uses write access:
  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • SELF
Where SELF is the creating user's SID. This is a pretty permissive SD. One interesting thing about RPC endpoints is they are multiplexed. You don't explicit associate an endpoint with the RPC interface you want to access. Instead you can connect to any endpoint that the process has created. The end result is that if there's a less secure endpoint in the same process it might be possible to access an interface using the least secure one. In general this makes relying on endpoint security risky, especially in processes which run multiple services, such as LSASS. In any case if you want to use a TCP endpoint you can't rely on the endpoint security as it doesn't exist.

Securing the Interface

The next way of securing the RPC server is to secure the interface itself. You register the interface structure that was generated by MIDL using one of the following APIs:
Each has a varying number of parameters some of which determine the security of the interface. The latest APIs are RpcServerRegisterIf3 and RpcServerInterfaceGroupCreate which were introduced in Windows 8. The latter is just a way of registering multiple interfaces in one call so we'll just focus on the former. The RpcServerRegisterIf3 has three parameters which affect security, SecurityDescriptor, IfCallback and Flags. 

The SecurityDescriptor parameter is easiest to explain. It assigns an SD to the interface, when a call is made on that interface then the caller's token is checked against the SD and access is only granted if the check passes. If no SD is specified a default is used which grants the following SIDs access (assuming a non-AppContainer process)
  • NT AUTHORITY\ANONYMOUS LOGON
  • Everyone
  • NT AUTHORITY\RESTRICTED
  • BUILTIN\Administrators
  • SELF
The token to use for the access check is based either on the client's authentication (we'll discuss this later) or the authentication for the endpoint. ALPC and named pipe are authenticated transports, where as TCP is not. When using an unauthenticated transport the access check will be against the anonymous token. This means if the SD does not contain an allow ACE for ANONYMOUS LOGON it will be blocked.

Note, due to a quirk of the access check process the RPC runtime grants access if the caller has any access granted, not a specific access right. What this means is that if the caller is considered the owner, which is normally set to the creating user SID they might only be granted READ_CONTROL but that's sufficient to bypass the check. This could also be useful if the caller has SeTakeOwnershipPrivilege or similar as it'd be possible to generically bypass the interface SD check (though of course that privilege is dangerous in its own right).

The second parameter, IfCallback, takes an RPC_IF_CALLBACK function pointer. This callback function will be invoked when a call is made to the interface, although it will be called after the SD is checked. If the callback function returns RPC_S_OK then the call will be allowed, anything else will deny the call. The callback gets a pointer to the interface and the binding handle and can do various checks to determine if the caller is allowed to access the interface.

A common check is for the client's authentication level. The client can specify the level to use when connecting to the server using the RpcBindingSetAuthInfo API however the server can't directly specify the minimum authentication level it accepts. Instead the callback can use the RpcBindingInqAuthClient API to determine what the client used and grant or deny access based on that. The authentication levels we typically care about are as follows:
  • RPC_C_AUTHN_LEVEL_NONE - No authentication
  • RPC_C_AUTHN_LEVEL_CONNECT - Authentication at connect time, but not per-call.
  • RPC_C_AUTHN_LEVEL_PKT_INTEGRITY - Authentication at connect time, each call has integrity protection.
  • RPC_C_AUTHN_LEVEL_PKT_PRIVACY - Authentication at connect time, each call is encrypted and has integrity protection.
The authentication is implemented using a defined authentication service, such as NTLM or Kerberos, though that doesn't really matter for our purposes. Also note that this is only used for RPC services available over remote protocols such as named pipes or TCP. If the RPC server listens on ALPC then it's assumed to always be RPC_C_AUTHN_LEVEL_PKT_PRIVACY. Other checks the server could do would be the protocol sequence the client used, this would allow rejecting access via TCP but permit named pipes.

The final parameter is the flags. The flag most obviously related to security is RPC_IF_ALLOW_SECURE_ONLY (0x8). This blocks access to the interface if the current authentication level is RPC_C_AUTHN_LEVEL_NONE. This means the caller must be able to authenticate to the server using one of the permitted authentication services. It's not sufficient to use a NULL session, at least on any modern version of Windows. Of course this doesn't say much about who has authenticated, a server might still want to check the caller's identity.

The other important flag is RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH (0x10). If the server specifies a security callback and this flag is not set then any unauthenticated client will be automatically rejected. 

If this wasn't complex enough there's at least one other related setting which applies system wide which will determine what type of clients can access what RPC server. The Restrict Unauthenticated RPC Clients group policy. By default this is set to None if the RPC server is running on a server SKU of Windows and Authenticated on a client SKU. 

In general what this policy does is limit whether a client can use an unauthenticated transport such as TCP when they haven't also separately authenticated to an valid authentication level. When set to None RPC servers can be accessed via an unauthenticated transport subject to any other restrictions the interface is registered with. If set to Authenticated then calls over unauthenticated transports are rejected, unless the RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH flag is set for the interface or the client has authenticated separately. There's a third option, Authenticated without exceptions, which will block the call in all circumstances if the caller isn't using an authenticated transport. 

Ad-hoc Security

The final types of checks are basically anything else the server does to verify the caller. A common approach would be to perform a check within a specific function on the interface. For example, a server could generally allow unauthenticated clients, except when calling a method to read a important secret value. At that point is could insert an authentication level check to ensure the client has authenticated at RPC_C_AUTHN_LEVEL_PKT_PRIVACY so that the secret will be encrypted when returned to the client. 

Ultimately you'll have to check each function you're interested in to determine what, if any, security checks are in place. As with all ad-hoc checks it's possible that there's a logic bug in there which can be exploited to bypass the security restrictions.

Digging into EFSRPC

Okay, that covers the basics of how an RPC server is secured. Let's look at the specific example of the EFSRPC server abused by PetitPotam. Oddly there's two implementation of the RPC server, one in efslsaext.dll which the interface UUID of c681d488-d850-11d0-8c52-00c04fd90f7e and one in efssvc.dll with the interface UUID of df1941c5-fe89-4e79-bf10-463657acf44d. The one in efslsaext.dll is the one which is accessible unauthenticated, so let's start there. We'll go through the three approaches to securing the server to determine what it's doing.

First, the server does not register any of its own protocol sequences, with SDs or not. What this means is who can call the RPC server is dependent on what other endpoints have been registered by the hosting process, which in this case is LSASS.

Second, checking the for calls to one of the RPC server interface registration functions there's a single call to RpcServerRegisterIfEx in InitializeLsaExtension. This allows the caller to specify the security callback but not an SD. However in this case it doesn't specify any security callback. The InitializeLsaExtension function also does not specify either of the two security flags (it sets RPC_IF_AUTOLISTEN which doesn't have any security impact). This means that in general any authenticated caller is permitted.

Finally, from an ad-hoc security perspective all the main functions such as EfsRpcOpenFileRaw call the function EfsRpcpValidateClientCall which looks something like the following (error check removed).

void EfsRpcpValidateClientCall(RPC_BINDING_HANDLE Binding, 
                               PBOOL ValidClient) {
  unsigned int ClientLocalFlag;
  I_RpcBindingIsClientLocal(NULL, &ClientLocalFlag);
  if (!ClientLocalFlag) {
    RPC_WSTR StringBinding;
    RpcBindingToStringBindingW(Binding, &StringBinding);
    RpcStringBindingParseW(StringBinding, NULL, &Protseq, 
                           NULL, NULL, NULL);
    if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, 
        Protseq, -1, L"ncacn_np", -1) == CSTR_EQUAL)
        *ValidClient = TRUE;
    }
  }
}

Basically the ValidClient parameter will only be set to TRUE if the caller used the named pipe transport and the pipe wasn't opened locally, i.e. the named pipe was opened over SMB. This is basically all the security that's being checked for. Therefore the only security that could be enforced is limited by who's allowed to connect to a suitable named pipe endpoint.

At a minimum LSASS registers the \pipe\lsass named pipe endpoint. When it's setup in lsasrv.dll a SD is defined for the named pipe that grants the following users access:
  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • BUILTIN\Administrators
Therefore in theory the anonymous user has access to the pipe, and as there are no other security checks in place in the interface definition. Now typically anonymous access isn't granted by default to named pipes via a NULL session, however domain controllers have an exception to this policy through the configured Network access: Named Pipes that can be accessed anonymously security option. For DCs this allows lsarpc, samr and netlogon pipes, which are all aliases for the lsass pipe, to be accessed anonymously.

You can now understand why the EFS RPC server is accessible anonymously on DCs. How does the other EFS RPC server block access? In that case it specifies an interface SD to limit access to only the Everyone group and BUILTIN\Administrators. By default the anonymous user isn't a member of Everyone (although it can be configured as such) therefore this blocks access even if you connected via the lsass pipe.

The Fix is In

What did Microsoft do to fix PetitPotam? One thing they definitely didn't do is change the interface registration or the named pipe endpoint security. Instead they added an additional ad-hoc check to EfsRpcOpenFileRaw. Specifically they added the following code:

DWORD AllowOpenRawDL = 0;
RegGetValueW(
  HKEY_LOCAL_MACHINE,
  L"SYSTEM\\CurrentControlSet\\Services\\EFS",
  L"AllowOpenRawDL",
  RRF_RT_REG_DWORD | RRF_ZEROONFAILURE,
  NULL,
  &AllowOpenRawDL);
if (AllowOpenRawDL == 1 && 
    !EfsRpcpValidateClientCall(hBinding, &ValidClient) && ValidClient) {
  // Call allowed.
}

Basically unless the AllowOpenRawDL registry value is set to one then the call is blocked entirely regardless of the authenticating client. This seems to be a perfectly valid fix, except that EfsRpcOpenFileRaw isn't the only function usable to start an NTLM authentication session. As pointed out by Lee Christensen you can also do it via EfsRpcEncryptFileSrv or EfsRpcQueryUsersOnFile or others. Therefore as no other changes were put in place these other functions are accessible just as unauthenticated as the original.

It's really unclear how Microsoft didn't see this, but I guess they might have been blinded by them actually fixing something which they were adamant was a configuration issue that sysadmins had to deal with. 

UPDATE 2021/08/17: It's worth noting that while you can access the other functions unauthenticated it seems any network access is done using the "authenticated" caller, i.e. the ANONYMOUS user so it's probably not that useful. The point of this blog is not about abusing EFSRPC but why it's abusable :-)

Anyway I hope that explains why PetitPotam works unauthenticated (props to topotam77 for the find) and might give you some insight into how you can determine what RPC servers might be accessible going forward. 

How the Windows Firewall RPC Filter Works

By: tiraniddo
22 August 2021 at 05:32

I did promise that I'd put out a blog post on how the Windows RPC filter works. Now that I released my more general blog post on the Windows firewall I thought I'd come back to a shorter post about the RPC filter itself. If you don't know the context, the Windows firewall has the ability to restrict access to RPC interfaces. This is interesting due to the renewed interest in all things RPC, especially the PetitPotam trick. For example you can block any access to the EFSRPC interfaces using the following script which you run with the netsh command.

rpc
filter
add rule layer=um actiontype=block
add condition field=if_uuid matchtype=equal data=c681d488-d850-11d0-8c52-00c04fd90f7e
add filter
add rule layer=um actiontype=block
add condition field=if_uuid matchtype=equal data=df1941c5-fe89-4e79-bf10-463657acf44d
add filter
quit

This script adds two rules which will block any calls on the RPC interfaces with UUIDs of c681d488-d850-11d0-8c52-00c04fd90f7e and df1941c5-fe89-4e79-bf10-463657acf44d. These correspond to the two EFSRPC interfaces.

How does this work within the context of the firewall? Does the kernel components of the Windows Filtering Platform have a builtin RPC protocol parser to block the connection? That'd be far too complex, instead everything is done in user-mode by some special layers. If you use NtObjectManager's firewall Get-FwLayer command you can check for layers registered to run in user-mode by filtering on the IsUser property.

PS> Get-FwLayer | Where-Object IsUser
KeyName                      Name
-------                      ----
FWPM_LAYER_RPC_PROXY_CONN    RPC Proxy Connect Layer
FWPM_LAYER_IPSEC_KM_DEMUX_V4 IPsec KM Demux v4 Layer
FWPM_LAYER_RPC_EP_ADD        RPC EP ADD Layer
FWPM_LAYER_KM_AUTHORIZATION  Keying Module Authorization Layer
FWPM_LAYER_IKEEXT_V4         IKE v4 Layer
FWPM_LAYER_IPSEC_V6          IPsec v6 Layer
FWPM_LAYER_IPSEC_V4          IPsec v4 Layer
FWPM_LAYER_IKEEXT_V6         IKE v6 Layer
FWPM_LAYER_RPC_UM            RPC UM Layer
FWPM_LAYER_RPC_PROXY_IF      RPC Proxy Interface Layer
FWPM_LAYER_RPC_EPMAP         RPC EPMAP Layer
FWPM_LAYER_IPSEC_KM_DEMUX_V6 IPsec KM Demux v6 Layer

In the output we can see 5 layers with RPC in the name of the layer. 
  • FWPM_LAYER_RPC_EP_ADD - Filter new endpoints created by a process.
  • FWPM_LAYER_RPC_EPMAP - Filter access to endpoint mapper information.
  • FWPM_LAYER_RPC_PROXY_CONN - Filter connections to the RPC proxy.
  • FWPM_LAYER_RPC_PROXY_IF - Filter interface calls through an RPC proxy.
  • FWPM_LAYER_RPC_UM - Filter interface calls to an RPC server
Each of these layers is potentially interesting, and you can add rules through netsh for all of them. But we'll just focus on how the FWPM_LAYER_RPC_UM layer works as that's the one the script introduced at the start works with. If you run the following command after adding the RPC filter rules you can view the newly created rules:

PS> Get-FwFilter -LayerKey FWPM_LAYER_RPC_UM -Sorted | Format-FwFilter
Name       : RPCFilter
Action Type: Block
Key        : d4354417-02fa-11ec-95da-00155d010a06
Id         : 78253
Description: RPC Filter
Layer      : FWPM_LAYER_RPC_UM
Sub Layer  : FWPM_SUBLAYER_UNIVERSAL
Flags      : Persistent
Weight     : 567453553048682496
Conditions :
FieldKeyName               MatchType Value
------------               --------- -----
FWPM_CONDITION_RPC_IF_UUID Equal     df1941c5-fe89-4e79-bf10-463657acf44d


Name       : RPCFilter
Action Type: Block
Key        : d4354416-02fa-11ec-95da-00155d010a06
Id         : 78252
Description: RPC Filter
Layer      : FWPM_LAYER_RPC_UM
Sub Layer  : FWPM_SUBLAYER_UNIVERSAL
Flags      : Persistent
Weight     : 567453553048682496
Conditions :
FieldKeyName               MatchType Value
------------               --------- -----
FWPM_CONDITION_RPC_IF_UUID Equal     c681d488-d850-11d0-8c52-00c04fd90f7e

If you're read my general blog post the output should made some sense. The FWPM_CONDITION_RPC_IF_UUID condition key is used to specify the UUID for the interface to match on. The FWPM_LAYER_RPC_UM has many possible fields to filter on, which you can query by inspecting the layer object's Fields property.

PS> (Get-FwLayer -Key FWPM_LAYER_RPC_UM).Fields

KeyName                              Type      DataType
-------                              ----      --------
FWPM_CONDITION_REMOTE_USER_TOKEN     RawData   TokenInformation
FWPM_CONDITION_RPC_IF_UUID           RawData   ByteArray16
FWPM_CONDITION_RPC_IF_VERSION        RawData   UInt16
FWPM_CONDITION_RPC_IF_FLAG           RawData   UInt32
FWPM_CONDITION_DCOM_APP_ID           RawData   ByteArray16
FWPM_CONDITION_IMAGE_NAME            RawData   ByteBlob
FWPM_CONDITION_RPC_PROTOCOL          RawData   UInt8
FWPM_CONDITION_RPC_AUTH_TYPE         RawData   UInt8
FWPM_CONDITION_RPC_AUTH_LEVEL        RawData   UInt8
FWPM_CONDITION_SEC_ENCRYPT_ALGORITHM RawData   UInt32
FWPM_CONDITION_SEC_KEY_SIZE          RawData   UInt32
FWPM_CONDITION_IP_LOCAL_ADDRESS_V4   IPAddress UInt32
FWPM_CONDITION_IP_LOCAL_ADDRESS_V6   IPAddress ByteArray16
FWPM_CONDITION_IP_LOCAL_PORT         RawData   UInt16
FWPM_CONDITION_PIPE                  RawData   ByteBlob
FWPM_CONDITION_IP_REMOTE_ADDRESS_V4  IPAddress UInt32
FWPM_CONDITION_IP_REMOTE_ADDRESS_V6  IPAddress ByteArray16

There's quite a few potential configuration options for the filter. You can filter based on the remote user token that's authenticated to the interface. Or you can filters based on the authentication level and type. This could allow you to protect an RPC interface so that all callers have to use Kerberos with at RPC_C_AUTHN_LEVEL_PKT_PRIVACY level. 

Anyway, configuring it is less important to us, you probably want to know how it works, as the first step to trying to find a way to bypass it is to know where this filter layer is processed (note, I've not found a bypass, but you never know). 

Perhaps unsurprisingly due to the complexity of the RPC protocol the filtering is implemented within the RPC server process through the RpcRtRemote extension DLL. Except for RPCSS this DLL isn't loaded by default. Instead it's only loaded if there exists a value for the WNF_RPCF_FWMAN_RUNNING WNF state. The following shows the state after adding the two RPC filter rules with netsh.

PS> $wnf = Get-NtWnf -Name 'WNF_RPCF_FWMAN_RUNNING'
PS> $wnf.QueryStateData()

Data ChangeStamp
---- -----------
{}             2

The RPC runtime sets up a subscription to load the DLL if the WNF value is ever changed. Once loaded the RPC runtime will register all current interfaces to check the firewall. The filter rules are checked when a call is made to the interface during the normal processing of the security callback. The runtime will invoke the FwFilter function inside RpcRtRemote, passing all the details about the firewall interface call. The filter call is only made for DCE/RPC protocols, so not ALPC. It also will only be called if the caller is remote. This is always the case if the call comes via TCP, but for named pipes it will only be called if the pipe was opened via SMB.

Here's where we can finally determine how the RPC filter is processed. The FwFilter function builds a list of firewall values corresponding to the list of fields for the FWPM_LAYER_RPC_UM layer and passes them to the FwpsClassifyUser0 API along with the numeric ID of the layer. This API will enumerate all filters for the layer and apply the condition checks returning the classification, e.g. block or permit. Based on this classification the RPC runtime can permit or refuse the call. 

In order for a filter to be accessible for classification the RPC server must have FWPM_ACTRL_OPEN access to the engine and FWPM_ACTRL_CLASSIFY access to the filter. By default the Everyone group has these access rights, however AppContainers and potentially other sandboxes do not. However, in general AppContainer processes don't tend to create privileged RPC servers, at least any which a remote attacker would find useful. You can check the access on various firewall objects using the Get-AccessibleFwObject command.

PS> $token = Get-NtToken -Filtered -Flags LuaToken
PS> Get-AccessibleFwObject -Token $token | Where-Object Name -eq RPCFilter

TokenId Access             Name
------- ------             ----
4ECF80  Classify|Open RPCFilter
4ECF80  Classify|Open RPCFilter

I hope this gives enough information for someone to dig into it further to see if there's any obvious bypass I missed. I'm sure there's probably some fun trick you could do to circumvent restrictions if you look hard enough :-)

LowBox Token Permissive Learning Mode

By: tiraniddo
7 September 2021 at 06:53

I was recently asked about this topic and so I thought it'd make sense to put it into a public blog post so that everyone can benefit. Windows 11 (and Windows Server 2022) has a new feature for tokens which allow the kernel to perform the normal LowBox access check, but if it fails log the error rather than failing with access denied. 

This feature allows you to start an AppContainer sandbox process, run a task, and determine what parts of that would fail if you actually tried to sandbox a process. This makes it much easier to determine what capabilities you might need to grant to prevent your application from crashing if you tried to actually apply the sandbox. It's a very useful diagnostic tool, although whether it'll be documented by Microsoft remains to be seen. Let's go through a quick example of how to use it.

First you need to start an ETW trace for the Microsoft-Windows-Kernel-General provider with the KERNEL_GENERAL_SECURITY_ACCESSCHECK keyword (value 0x20) enabled. In an administrator PowerShell console you can run the following:

PS> $name = 'AccessTrace'
PS> New-NetEventSession -Name $name -LocalFilePath "$env:USERPROFILE\access_trace.etl" | Out-Null
PS> Add-NetEventProvider -SessionName $name -Name "Microsoft-Windows-Kernel-General" -MatchAllKeyword 0x20 | Out-Null
PS> Start-NetEventSession -Name $name

This will start the trace session and log the events to access_trace.etl file if your home directory. As this is ETW you could probably do a real-time trace or enable stack tracing to find out what code is actually failing, however for this example we'll do the least amount of work possible. This log is also used for things like Adminless which I've blogged about before.

Now you need to generate some log events. You just need to add the permissiveLearningMode capability when creating the lowbox token or process. You can almost certainly add it to your application's manifest as well when developing a sandboxed UWP application, but we'll assume here that we're setting up the sandbox manually.

PS> $cap = Get-NtSid -CapabilityName 'permissiveLearningMode'
PS> $token = Get-NtToken -LowBox -PackageSid ABC -CapabilitySid $cap
PS> Invoke-NtToken $token { "Hello" | Set-Content "$env:USERPOFILE\test.txt" }

The previous code creates a lowbox token with the capability and writes to a file in the user's profile. This would normally fail as the user's profile doesn't grant any AppContainer access to write to it. However, you should find the write succeeded. Now, back in the admin PowerShell console you'll want to stop the trace and cleanup the session.

PS> Stop-NetEventSession -Name $name
PS> Remove-NetEventSession -Name $name

You should find an access_trace.etl file in your user's profile directory which will contain the logged events. There are various ways to read this file, the simplest is to use the Get-WinEvent command. As you need to do a bit of parsing of the contents of the log to get out various values I've put together a simple script do that. It's available on github here. Just run the script passing the name of the log file to convert the events into PowerShell objects.

PS> parse_access_check_log.ps1 "$env:USERPROFILE\access_trace.etl"
ProcessName        : ...\v1.0\powershell.exe
Mask               : MaximumAllowed
PackageSid         : S-1-15-2-1445519891-4232675966-...
Groups             : INSIDERDEV\user
Capabilities       : NAMED CAPABILITIES\Permissive Learning Mode
SecurityDescriptor : O:BAG:BAD:(A;OICI;KA;;;S-1-5-21-623841239-...

The log events don't seem to contain the name of the resource being opened, but it does contain the security descriptor and type of the object, what access mask was requested and basic information about the access token used. Hopefully this information is useful to someone.

Bypassing UAC in the most Complex Way Possible!

By: tiraniddo
20 March 2022 at 09:52

While it's not something I spend much time on, finding a new way to bypass UAC is always amusing. When reading through some of the features of the Rubeus tool I realised that there was a possible way of abusing Kerberos to bypass UAC, well on domain joined systems at least. It's unclear if this has been documented before, this post seems to discuss something similar but relies on doing the UAC bypass from another system, but what I'm going to describe works locally. Even if it has been described as a technique before I'm not sure it's been documented how it works under the hood.

The Background!

Let's start with how the system prevents you bypassing the most pointless security feature ever. By default LSASS will filter any network authentication tokens to remove admin privileges if the users is a local administrator. However there's an important exception, if the user a domain user and a local administrator then LSASS will allow the network authentication to use the full administrator token. This is a problem if say you're using Kerberos to authenticate locally. Wouldn't this be a trivial UAC bypass? Just authenticate to the local service as a domain user and you'd get the network token which would bypass the filtering?

Well no, Kerberos has specific additions to block this attack vector. If I was being charitable I'd say this behaviour also ensures some level of safety.  If you're not running as the admin token then accessing say the SMB loopback interface shouldn't suddenly grant you administrator privileges through which you might accidentally destroy your system.

Back in January last year I read a post from Steve Syfuhs of Microsoft on how Kerberos prevents this local UAC bypass. The TL;DR; is when a user wants to get a Kerberos ticket for a service LSASS will send a TGS-REQ request to the KDC. In the request it'll embed some security information which indicates the user is local. This information will be embedded in the generated ticket. 

When that ticket is used to authenticate to the same system Kerberos can extract the information and see if it matches one it knows about. If so it'll take that information and realize that the user is not elevated and filter the token appropriately. Unfortunately much as enjoy Steve's posts this one was especially light on details. I guessed I'd have to track down how it works myself. Let's dump the contents of a Kerberos ticket and see if we can see what could be the ticket information:

PS> $c = New-LsaCredentialHandle -Package 'Kerberos' -UseFlag Outbound
PS> $x = New-LsaClientContext -CredHandle $c -Target HOST/$env:COMPUTERNAME
PS> $key = Get-KerberosKey -HexKey 'XXX' -KeyType AES256_CTS_HMAC_SHA1_96 -Principal $env:COMPTUERNAME
PS> $u = Unprotect-LsaAuthToken -Token $x.Token -Key $key
PS> Format-LsaAuthToken $u

<KerberosV5 KRB_AP_REQ>
Options         : None
<Ticket>
Ticket Version  : 5
...

<Authorization Data - KERB_AD_RESTRICTION_ENTRY>
Flags           : LimitedToken
Integrity Level : Medium
Machine ID      : 6640665F...

<Authorization Data - KERB_LOCAL>
Security Context: 60CE03337E01000025FC763900000000

I've highlighted the two ones of interest, the KERB-AD-RESTRICTION-ENTRY and the KERB-LOCAL entry. Of course I didn't guess these names, these are sort of documented in the Microsoft Kerberos Protocol Extensions (MS-KILE) specification. The KERB_AD_RESTRICTION_ENTRY is most obviously of interest, it contains both the works "LimitedToken" and "Medium Integrity Level"

When accepting a Kerberos AP-REQ from a network client via SSPI the Kerberos module in LSASS will call the LSA function LsaISetSupplementalTokenInfo to apply the information from KERB-AD-RESTRICTION-ENTRY to the token if needed. The pertinent code is roughly the following:

NTSTATUS LsaISetSupplementalTokenInfo(PHANDLE phToken, 
                        PLSAP_TOKEN_INFO_INTEGRITY pTokenInfo) {
  // ...
  BOOL bLoopback = FALSE:
  BOOL bFilterNetworkTokens = FALSE;

  if (!memcmp(&LsapGlobalMachineID, pTokenInfo->MachineID,
       sizeof(LsapGlobalMachineID))) {
    bLoopback = TRUE;
  }

  if (LsapGlobalFilterNetworkAuthenticationTokens) {
    if (pTokenInfo->Flags & LimitedToken) {
      bFilterToken = TRUE;
    }
  }

  PSID user = GetUserSid(*phToken);
  if (!RtlEqualPrefixSid(LsapAccountDomainMemberSid, user)
    || LsapGlobalLocalAccountTokenFilterPolicy 
    || NegProductType == NtProductLanManNt) {
    if ( !bFilterToken && !bLoopback )
      return STATUS_SUCCESS;
  }

  /// Filter token if needed and drop integrity level.
}

I've highlighted the three main checks in this function, the first compares if the MachineID field of the KERB-AD-RESTRICTION-ENTRY matches the one stored in LSASS. If it is then the bLoopback flag is set. Then it checks an AFAIK undocumented LSA flag to filter all network tokens, at which point it'll check for the LimitedToken flag and set the bFilterToken flag accordingly. This filtering mode defaults to off so in general bFilterToken won't be set.

Finally the code queries for the current created token SID and checks if any of the following is true:
  • The user SID is not a member of the local account domain.
  • The LocalAccountTokenFilterPolicy LSA policy is non-zero, which disables the local account filtering.
  • The product type is NtProductLanManNt, which actually corresponds to a domain controller.
If any are true then as long as the token information is neither loopback or filtering is forced the function will return success and no filtering will take place. Therefore in a default installation for a domain user to not be filtered comes down whether the machine ID matches or not. 

For the integrity level, if filtering is taking place then it will be dropped to the value in the KERB-AD-RESTRICTION-ENTRY authentication data. However it won't increase the integrity level above what the created token has by default, so this can't be abused to get System integrity.

Note Kerberos will call LsaISetSupplementalTokenInfo with the KERB-AD-RESTRICTION-ENTRY authentication data from the ticket in the AP-REQ first. If that doesn't exist then it'll try calling it with the entry from the authenticator. If neither the ticket or authenticator has an entry then it will never be called. How can we remove these values?

Well, about that!

Okay how can we abuse this to bypass UAC? Assuming you're authenticated as a domain user the funniest way to abuse it is get the machine ID check to fail. How would we do that? The LsapGlobalMachineID value is a random value generated when LSASS starts up. We can abuse the fact that if you query the user's local Kerberos ticket cache it will return the session key for service tickets even if you're not an administrator (it won't return TGT session keys by default).

Therefore one approach is to generate a service ticket for the local system, save the resulting KRB-CRED to disk, reboot the system to get LSASS to reinitialize and then when back on the system reload the ticket. This ticket will now have a different machine ID and therefore Kerberos will ignore the restrictions entry. You could do it with the builtin klist and Rubeus with the following commands:

PS> klist get RPC/$env:COMPUTERNAME
PS> Rubeus.exe /dump /server:$env:COMPUTERNAME /nowrap
... Copy the base64 ticket to a file.

Reboot then:

PS> Rubeus.exe ptt /ticket:<BASE64 TICKET> 

You can use Kerberos authentication to access the SCM over named pipes or TCP using the RPC/HOSTNAME SPN.  Note the Win32 APIs for the SCM always use Negotiate authentication which throws a spanner in the works, but there are alternative RPC clients ;-) While LSASS will add a valid restrictions entry to the authenticator in the AP-REQ it won't be used as the one in the ticket will be used first which will fail to apply due to the different machine ID.

The other approach is to generate our own ticket, but won't we need credentials for that? There's a trick, I believe discovered by Benjamin Delpy and put into kekeo that allows you to abuse unconstrained delegation to get a local TGT with a session key. With this TGT you can generate your own service tickets, so you can do the following:
  1. Query for the user's TGT using the delegation trick.
  2. Make a request to the KDC for a new service ticket for the local machine using the TGT. Add a KERB-AD-RESTRICTION-ENTRY but fill in a bogus machine ID.
  3. Import the service ticket into the cache.
  4. Access the SCM to bypass UAC.
Ultimately this is a reasonable amount lot of code for a UAC bypass, at least compared to the just changing an environment variable. However, you can probably bodge it together using existing tools such as kekeo and Rubeus, but I'm not going to release a turn key tool to do this, you're on your own :-)

Didn't you forget KERB-LOCAL?

What is the purpose of KERB-LOCAL? It's a way of reusing the local user's credentials, this is similar to NTLM loopback where LSASS is able to determine that the call is actually from a locally authenticated user and use their interactive token. The value passed in the ticket and authenticator can be checked against a list of known credentials in the Kerberos package and if there's a match the existing token will be used.

Would this not always eliminate the need for the filtering the token based on the KERB-AD-RESTRICTION-ENTRY value? It seems that this behavior is used very infrequently due to how it's designed. First it only works if the accepting server is using the Negotiate package, it doesn't work if using the Kerberos package directly (sort of...). That's usually not an impediment as most local services use Negotiate anyway for convenience. 

The real problem is that as a rule if you use Negotiate to the local machine as a client it'll select NTLM as the default. This will use the loopback already built into NTLM rather than Kerberos so this feature won't be used. Note that even if NTLM is disabled globally on the domain network it will still work for local loopback authentication. I guess KERB-LOCAL was added for feature parity with NTLM.

Going back to the formatted ticket at the start of the blog what does the KERB-LOCAL value mean? It can be unpacked into two 64bit values, 0x17E3303CE60 and 0x3976FC25. The first value is the heap address of the KERB_CREDENTIAL structure in LSASS's heap!! The second value is the ticket count when the KERB-LOCAL structure was created.

Fortunately LSSAS doesn't just dereference the credentials pointer, it must be in the list of valid credential structures. But the fact that this value isn't blinded or references a randomly generated value seems a mistake as heap addresses would be fairly easy to brute force. Of course it's not quite so simple, Kerberos does verify that the SID in the ticket's PAC matches the SID in the credentials so you can't just spoof the SYSTEM session, but well, I'll leave that as a thought to be going on with.

Hopefully this gives some more insight into how this feature works and some fun you can have trying to bypass UAC in a new way.

UPDATE: This simple C++ file can be used to modify the Win32 SCM APIs to use Kerberos for local authentication.

Exploiting RBCD Using a Normal User Account*

By: tiraniddo
14 May 2022 at 02:29

* Caveats apply.

Resource Based Constrained Delegate (RBCD) privilege escalation, described by Elad Shamir in the "Wagging the Dog" blog post is a devious way of exploiting Kerberos to elevate privileged on a local  Windows machine. All it requires is write access to local computer's domain account to modify the msDS-AllowedToActOnBehalfOfOtherIdentity LDAP attribute to add another account's SID. You can then use that account with the Services For User (S4U) protocols to get a Kerberos service ticket for the local machine as any user on the domain including local administrators. From there you can create a new service or whatever else you need to do.

The key is how you write to the LDAP server under the local computer's domain account. There's been various approaches usually abusing authentication relay. For example, I described one relay vector which abused DCOM. Someone else has then put this together in a turnkey tool, KrbRelayUp

One additional criteria for this to work is having access to another computer account to perform the attack. Well this isn't strictly true, there's the Shadow Credentials attack which allows you to reuse the same local computer account, but in general you need a computer account you control. Normally this isn't a problem, as the DC allows normal users to create new computer accounts up to a limit set by the domain's ms-DS-MachineAccountQuota attribute value. This attribute defaults to 10, but an administrator could set it to 0 and block the attack, which is probably recommend.

But I wondered why this wouldn't work as a normal user. The msDS-AllowedToActOnBehalfOfOtherIdentity attribute just needs the SID for the account to be allowed to delegate to the computer. Why can't we just add the user's SID and perform the S4U dance? To give us the best chance I'll assume we have knowledge of a user's password, how you get this is entirely up to you. Running the attack through Rubeus shows our problem.

PS C:\> Rubeus.exe s4u /user:charlie /domain:domain.local /dc:primarydc.domain.local /rc4:79bf93c9501b151506adc21ba0397b33 /impersonateuser:Administrator /msdsspn:cifs/WIN10TEST.domain.local

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/
  v2.0.3
[*] Action: S4U
[*] Using rc4_hmac hash: 79bf93c9501b151506adc21ba0397b33
[*] Building AS-REQ (w/ preauth) for: 'domain.local\charlie'
[*] Using domain controller: 10.0.0.10:88
[+] TGT request successful!
[*] base64(ticket.kirbi):
      doIFc...
[*] Action: S4U
[*] Building S4U2self request for: '[email protected]'
[*] Using domain controller: primarydc.domain.local (10.0.0.10)
[*] Sending S4U2self request to 10.0.0.10:88
[X] KRB-ERROR (7) : KDC_ERR_S_PRINCIPAL_UNKNOWN
[X] S4U2Self failed, unable to perform S4U2Proxy.

We don't even get past the first S4U2Self stage of the attack, it fails with a KDC_ERR_S_PRINCIPAL_UNKNOWN error. This error typically indicates the KDC doesn't know what encryption key to use for the generated ticket. If you add an SPN to the user's account however it all succeeds. This would imply it's not a problem with a user account per-se, but instead just a problem of the KDC not being able to select the correct key.

Technically speaking there should be no reason that the KDC couldn't use the user's long term key if you requested a ticket for their UPN, but it doesn't (contrary to an argument I had on /r/netsec the other day with someone who was adamant that SPN's are a convenience, not a fundamental requirement of Kerberos). 

So what to do? There is a way of getting a ticket encrypted for a UPN by using the User 2 User (U2U) extension. Would this work here? Looking at the Rubeus code it seems requesting a U2U S4U2Self ticket is supported, but the parameters are not set for the S4U attack. Let's set those parameters to request a U2U ticket and see if it works.

[+] S4U2self success!
[*] Got a TGS for 'Administrator' to '[email protected]'
[*] base64(ticket.kirbi): doIF...bGll

[*] Impersonating user 'Administrator' to target SPN 'cifs/WIN10TEST.domain.local'
[*] Building S4U2proxy request for service: 'cifs/WIN10TEST.domain.local'
[*] Using domain controller: primarydc.domain.local (10.0.0.10)
[*] Sending S4U2proxy request to domain controller 10.0.0.10:88
[X] KRB-ERROR (13) : KDC_ERR_BADOPTION

Okay, we're getting closer. The S4U2Self request was successful, unfortunately the S4U2Proxy request was not, failing with a KDC_ERR_BADOPTION error. After a bit of playing around this is almost certainly because the KDC can't decrypt the ticket sent in the S4U2Proxy request. It'll try the user's long term key, but that will obviously fail. I tried to see if I could send the user's TGT with the request (in addition to the S4U2Self service ticket) but it still failed. Is this not going to be possible?

Thinking about this a bit more, I wondered, could I decrypt the S4U2Self ticket and then encrypt with the long term key I already know for the user? Technically speaking this would create a valid Kerberos ticket, however it wouldn't create a valid PAC. This is because the PAC contains a Server Signature which is a HMAC of the PAC using the key used to encrypt the ticket. The KDC checks this to ensure the PAC hasn't been modified or put into a new ticket, and if it's incorrect it'll fail the request.

As we know the key, we could just update this value. However, the Server Signature is protected by the KDC Signature which is a HMAC keyed with the KDC's own key. We don't know this key and so we can't update this second signature to match the modified Server Signature. Looks like we're stuck.

Still, what would happen if the user's long term key happened to match the TGT session key we used to encrypt the S4U2Self ticket? It's pretty unlikely to happen by chance, but with knowledge of the user's password we could conceivably change the user's password on the DC between the S4U2Self and the S4U2Proxy requests so that when submitting the ticket the KDC can decrypt it and perhaps we can successfully get the delegated ticket.

As we know the TGT's session key, one obvious approach would be to "crack" the hash value back to a valid Unicode password. For AES keys I think this is going to be difficult and even if successful could be time consuming. However, RC4 keys are just a MD4 hash with no additional protection against brute force cracking. Fortunately the code in Rubeus defaults to requesting an RC4 session key for the TGT, and MS have yet to disable RC4 by default in Windows domains. This seems like it might be doable, even if it takes a long time. We would also need the "cracked" password to be valid per the domain's password policy which adds extra complications.

However, I recalled when playing with the SAM RPC APIs that there is a SamrChangePasswordUser method which will change a user's password to an arbitrary NT hash. The only requirement is knowledge of the existing NT hash and we can set any new NT hash we like. This doesn't need to honor the password policy, except for the minimum age setting. We don't even need to deal with how to call the RPC API correctly as the SAM DLL exports the SamiChangePasswordUser API which does all the hard work. 

I took some example C# code written by Vincent Le Toux and plugged that into Rubeus at the correct point, passing the current TGT's session key as the new NT hash. Let's see if it works:

SamConnect OK
SamrOpenDomain OK
rid is 1208
SamOpenUser OK
SamiChangePasswordUser OK

[*] Impersonating user 'Administrator' to target SPN 'cifs/WIN10TEST.domain.local'
[*] Building S4U2proxy request for service: 'cifs/WIN10TEST.domain.local'
[*] Using domain controller: primarydc.domain.local (10.0.0.10)
[*] Sending S4U2proxy request to domain controller 10.0.0.10:88
[+] S4U2proxy success!
[*] base64(ticket.kirbi) for SPN 'cifs/WIN10TEST.domain.local':
      doIG3...

And it does! Now the caveats:

  • This will obviously only work if RC4 is still enabled on the domain. 
  • You will need the user's password or NT hash. I couldn't think of a way of doing this with only a valid TGT.
  • The user is sacrificial, it might be hard to login using a password afterwards. If you can't immediately reset the password due to the domain's policy the user might be completely broken. 
  • It's not very silent, but that's not my problem.
  • You're probably better to just do the shadow credentials attack, if PKINIT is enabled.
As I'm feeling lazy I'm not going to provide the changes to Rubeus. Except for the call to SamiChangePasswordUser all the code is already there to perform the attack, it just needs to be wired up. I'm sure they'd welcome the addition.

Finding Running RPC Server Information with NtObjectManager

By: tiraniddo
26 June 2022 at 21:56

When doing security research I regularly use my NtObjectManager PowerShell module to discover and call RPC servers on Windows. Typically I'll use the Get-RpcServer command, passing the name of a DLL or EXE file to extract the embedded RPC servers. I can then use the returned server objects to create a client to access the server and call its methods. A good blog post about how some of this works was written recently by blueclearjar.

Using Get-RpcServer only gives you a list of what RPC servers could possibly be running, not whether they are running and if so in what process. This is where the RpcView does better, as it parses a process' in-memory RPC structures to find what is registered and where. Unfortunately this is something that I'm yet to implement in NtObjectManager

However, it turns out there's various ways to get the running RPC server information which are provided by OS and the RPC runtime which we can use to get a more or less complete list of running servers. I've exposed all the ones I know about with some recent updates to the module. Let's go through the various ways you can piece together this information.

NOTE some of the examples of PowerShell code will need a recent build of the NtObjectManager module. For various reasons I've not been updating the version of the PS gallery, so get the source code from github and build it yourself.

RPC Endpoint Mapper

If you're lucky this is simplest way to find out if a particular RPC server is running. When an RPC server is started the service can register an RPC interface with the function RpcEpRegister specifying the interface UUID and version along with the binding information with the RPC endpoint mapper service running in RPCSS. This registers all current RPC endpoints the server is listening on keyed against the RPC interface. 

You can query the endpoint table using the RpcMgmtEpEltInqBegin and RpcMgmtEpEltInqNext APIs. I expose this through the Get-RpcEndpoint command. Running Get-RpcEndpoint with no parameters returns all interfaces the local endpoint mapper knows about as shown below.

PS> Get-RpcEndpoint
UUID                                 Version Protocol     Endpoint      Annotation
----                                 ------- --------     --------      ----------
51a227ae-825b-41f2-b4a9-1ac9557a1018 1.0     ncacn_ip_tcp 49669         
0497b57d-2e66-424f-a0c6-157cd5d41700 1.0     ncalrpc      LRPC-5f43...  AppInfo
201ef99a-7fa0-444c-9399-19ba84f12a1a 1.0     ncalrpc      LRPC-5f43...  AppInfo
...

Note that in addition to the interface UUID and version the output shows the binding information for the endpoint, such as the protocol sequence and endpoint. There is also a free form annotation field, but that can be set to anything the server likes when it calls RpcEpRegister.

The APIs also allow you to specify a remote server hosting the endpoint mapper. You can use this to query what RPC servers are running on a remote server, assuming the firewall doesn't block you. To do this you'd need to specify a binding string for the SearchBinding parameter as shown.

PS> Get-RpcEndpoint -SearchBinding 'ncacn_ip_tcp:primarydc'
UUID                                 Version Protocol     Endpoint     Annotation
----                                 ------- --------     --------     ----------
d95afe70-a6d5-4259-822e-2c84da1ddb0d 1.0     ncacn_ip_tcp 49664
5b821720-f63b-11d0-aad2-00c04fc324db 1.0     ncacn_ip_tcp 49688
650a7e26-eab8-5533-ce43-9c1dfce11511 1.0     ncacn_np     \PIPE\ROUTER Vpn APIs
...

The big issue with the RPC endpoint mapper is it only contains RPC interfaces which were explicitly registered against an endpoint. The server could contain many more interfaces which could be accessible, but as they weren't registered they won't be returned from the endpoint mapper. Registration will typically only be used if the server is using an ephemeral name for the endpoint, such as a random TCP port or auto-generated ALPC name.

Pros:

  • Simple command to run to get a good list of running RPC servers.
  • Can be run against remote servers to find out remotely accessible RPC servers.
Cons:
  • Only returns the RPC servers intentionally registered.
  • Doesn't directly give you the hosting process, although the optional annotation might give you a clue.
  • Doesn't give you any information about what the RPC server does, you'll need to find what executable it's hosted in and parse it using Get-RpcServer.

Service Executable

If the RPC servers you extract are in a registered system service executable then the module will try and work out what service that corresponds to by querying the SCM. The default output from the Get-RpcServer command will show this as the Service column shown below.

PS> Get-RpcServer C:\windows\system32\appinfo.dll
Name        UUID                                 Ver Procs EPs Service Running
----        ----                                 --- ----- --- ------- -------
appinfo.dll 0497b57d-2e66-424f-a0c6-157cd5d41700 1.0 7     1   Appinfo True
appinfo.dll 58e604e8-9adb-4d2e-a464-3b0683fb1480 1.0 1     1   Appinfo True
appinfo.dll fd7a0523-dc70-43dd-9b2e-9c5ed48225b1 1.0 1     1   Appinfo True
appinfo.dll 5f54ce7d-5b79-4175-8584-cb65313a0e98 1.0 1     1   Appinfo True
appinfo.dll 201ef99a-7fa0-444c-9399-19ba84f12a1a 1.0 7     1   Appinfo True

The output also shows the appinfo.dll executable is the implementation of the Appinfo service, which is the general name for the UAC service. Note here that is also shows whether the service is running, but that's just for convenience. You can use this information to find what process is likely to be hosting the RPC server by querying for the service PID if it's running. 

PS> Get-Win32Service -Name Appinfo
Name    Status  ProcessId
----    ------  ---------
Appinfo Running 6020

The output also shows that each of the interfaces have an endpoint which is registered against the interface UUID and version. This is extracted from the endpoint mapper which makes it again only for convenience. However, if you pick an executable which isn't a service implementation the results are less useful:

PS> Get-RpcServer C:\windows\system32\efslsaext.dll
Name          UUID                   Ver Procs EPs Service Running      
----          ----                   --- ----- --- ------- -------      
efslsaext.dll c681d488-d850-11d0-... 1.0 21    0           False

The efslsaext.dll implements one of the EFS implementations, which are all hosted in LSASS. However, it's not a registered service so the output doesn't show any service name. And it's also not registered with the endpoint mapper so doesn't show any endpoints, but it is running.

Pros:

  • If the executable's a service it gives you a good idea of who's hosting the RPC servers and if they're currently running.
  • You can get the RPC server interface information along with that information.
Cons:
  • If the executable isn't a service it doesn't directly help.
  • It doesn't ensure the RPC servers are running if they're not registered in the endpoint mapper. 
  • Even if the service is running it might not have enabled the RPC servers.

Enumerating Process Modules

Extracting the RPC servers from an arbitrary executable is fine offline, but what if you want to know what RPC servers are running right now? This is similar to RpcView's process list GUI, you can look at a process and find all all the services running within it.

It turns out there's a really obvious way of getting a list of the potential services running in a process, enumerate the loaded DLLs using an API such as EnumerateLoadedModules, and then run Get-RpcServer on each one to extract the potential services. To use the APIs you'd need to have at least read access to the target process, which means you'd really want to be an administrator, but that's no different to RpcView's limitations.

The big problem is just because a module is loaded it doesn't mean the RPC server is running. For example the WinHTTP DLL has a built-in RPC server which is only loaded when running the WinHTTP proxy service, but the DLL could be loaded in any process which uses the APIs.

To simplify things I expose this approach through the Get-RpcServer function with the ProcessId parameter. You can also use the ServiceName parameter to lookup a service PID if you're interested in a specific service.

PS> Get-RpcEndpoint -ServiceName Appinfo
Name        UUID                        Ver Procs EPs Service Running                ----        ----                        --- ----- --- ------- -------
RPCRT4.dll  afa8bd80-7d8a-11c9-bef4-... 1.0 5     0           False
combase.dll e1ac57d7-2eeb-4553-b980-... 0.0 0     0           False
combase.dll 00000143-0000-0000-c000-... 0.0 0     0           False

Pros:

  • You can determine all RPC servers which could be potentially running for an arbitrary process.
Cons:
  • It doesn't ensure the RPC servers are running if they're not registered in the endpoint mapper. 
  • You can't directly enumerate the module list, except for the main executable, from a protected process (there's are various tricks do so, but out of scope here).

Asking an RPC Endpoint Nicely

The final approach is just to ask an RPC endpoint nicely to tell you what RPC servers is supports. We don't need to go digging into the guts of a process to do this, all we need is the binding string for the endpoint we want to query and then call the RpcMgmtInqIfIds API.

This will only return the UUID and version of the RPC server that's accessible from the endpoint, not the RPC server information. But it will give you an exact list of all supported RPC servers, in fact it's so detailed it'll give you all the COM interfaces that the process is listening on as well. To query this list you only need to access to the endpoint transport, not the process itself.

How do you get the endpoints though? One approach is if you do have access to the process you can enumerate its server ALPC ports by getting a list of handles for the process, finding the ports with the \RPC Control\ prefix in their name and then using that to form the binding string. This approach is exposed through Get-RpcEndpoint's ProcessId parameter. Again it also supports a ServiceName parameter to simplify querying services.

PS> Get-RpcEndpoint -ServiceName AppInfo
UUID              Version Protocol Endpoint     
----              ------- -------- --------  
0497b57d-2e66-... 1.0     ncalrpc  \RPC Control\LRPC-0ee3...
201ef99a-7fa0-... 1.0     ncalrpc  \RPC Control\LRPC-0ee3...
...

If you don't have access to the process you can do it in reverse by enumerating potential endpoints and querying each one. For example you could enumerate the \RPC Control object directory and query each one. Since Windows 10 19H1 ALPC clients can now query the server's PID, so you can not only find out the exposed RPC servers but also what process they're running in. To query from the name of an ALPC port use the AlpcPort parameter with Get-RpcEndpoint.

PS> Get-RpcEndpoint -AlpcPort LRPC-0ee3261d56342eb7ac
UUID              Version Protocol Endpoint     
----              ------- -------- --------  
0497b57d-2e66-... 1.0     ncalrpc  \RPC Control\LRPC-0ee3...
201ef99a-7fa0-... 1.0     ncalrpc  \RPC Control\LRPC-0ee3...
...

Pros:

  • You can determine exactly what RPC servers are running in a process.
Cons:
  • You can't directly determine what the RPC server does as the list gives you no information about which module is hosting it.

Combining Approaches

Obviously no one approach is perfect. However, you can get most of the way towards RpcView process list by combining the module enumeration approach with asking the endpoint nicely. For example, you could first get a list of potential interfaces by enumerating the modules and parsing the RPC servers, then filter that list to only the ones which are running by querying the endpoint directly. This will also get you a list of the ALPC server ports that the RPC server is running on so you can directly connect to it with a manually built client. And example script for doing this is on github.

We are still missing some crucial information that RpcView can access such as the interface registration flags from any approach. Still, hopefully that gives you a few ways to approach analyzing the RPC attack surface of the local system and determining what endpoints you can call.

Access Checking Active Directory

By: tiraniddo
17 July 2022 at 04:49

Like many Windows related technologies Active Directory uses a security descriptor and the access check process to determine what access a user has to parts of the directory. Each object in the directory contains an nTSecurityDescriptor attribute which stores the binary representation of the security descriptor. When a user accesses the object through LDAP the remote user's token is used with the security descriptor to determine if they have the rights to perform the operation they're requesting.

Weak security descriptors is a common misconfiguration that could result in the entire domain being compromised. Therefore it's important for an administrator to be able to find and remediate security weaknesses. Unfortunately Microsoft doesn't provide a means for an administrator to audit the security of AD, at least in any default tool I know of. There is third-party tooling, such as Bloodhound, which will perform this analysis offline but from reading the implementation of the checking they don't tend to use the real access check APIs and so likely miss some misconfigurations.

I wrote my own access checker for AD which is included in my NtObjectManager PowerShell module. I've used it to find a few vulnerabilities, such as CVE-2021-34470 which was an issue with Exchange's changes to AD. This works "online", as in you need to have an active account in the domain to run it, however AFAIK it should provide the most accurate results if what you're interested in what access an specific user has to AD objects. While the command is available in the module it's perhaps not immediately obvious how to use it an interpret the result, therefore I decide I should write a quick blog post about it.

A Complex Process

The access check process is mostly documented by Microsoft in [MS-ADTS]: Active Directory Technical Specification. Specifically in section 5.1.3. However, this leaves many questions unanswered. I'm not going to go through how it works in full either, but let me give a quick overview.  I'm going to assume you have a basic knowledge of the structure of the AD and its objects.

An AD object contains many resources that access might want to be granted or denied on for a particular user. For example you might want to allow the user to create only certain types of child objects, or only modify certain attributes. There are many ways that Microsoft could have implemented security, but they decided on extending the ACL format to introduce the object ACE. For example the ACCESS_ALLOWED_OBJECT_ACE structure adds two GUIDs to the normal ACCESS_ALLOWED_ACE

The first GUID, ObjectType indicates the type of object that the ACE applies to. For example this can be set to the schema ID of an attribute and the ACE will grant access to only that attribute nothing else. The second GUID, InheritedObjectType is only used during ACL inheritance. It represents the schema ID of the object's class that is allowed to inherit this ACE. For example if it's set to the schema ID of the computer class, then the ACE will only be inherited if such a class is created, it will not be if say a user object is created instead. We only need to care about the first of these GUIDs when doing an access check.

To perform an access check you need to use an API such as AccessCheckByType which supports checking the object ACEs. When calling the API you pass a list of object type GUIDs you want to check for access on. When processing the DACL if an ACE has an ObjectType GUID which isn't in the passed list it'll be ignored. Otherwise it'll be handled according to the normal access check rules. If the ACE isn't an object ACE then it'll also be processed.

If all you want to do is check if a local user has access to a specific object or attribute then it's pretty simple. Just get the access token for that user, add the object's GUID to the list and call the access check API. The resulting granted access can be one of the following specific access rights, not the names in parenthesis are the ones I use in the PowerShell module for simplicity:
  • ACTRL_DS_CREATE_CHILD (CreateChild) - Create a new child object
  • ACTRL_DS_DELETE_CHILD (DeleteChild) - Delete a child object
  • ACTRL_DS_LIST (List) - Enumerate child objects
  • ACTRL_DS_SELF (Self) - Grant a write-validated extended right
  • ACTRL_DS_READ_PROP (ReadProp) - Read an attribute
  • ACTRL_DS_WRITE_PROP (WriteProp) - Write an attribute
  • ACTRL_DS_DELETE_TREE (DeleteTree) - Delete a tree of objects
  • ACTRL_DS_LIST_OBJECT (ListObject) - List a tree of objects
  • ACTRL_DS_CONTROL_ACCESS (ControlAccess) - Grant a control extended right
You can also be granted standard rights such as READ_CONTROL, WRITE_DAC or DELETE which do what you'd expect them to do. However, if you want see what the maximum granted access on the DC would be it's slightly more difficult. We have the following problems:
  • The list of groups granted to a local user is unlikely to match what they're granted on the DC where the real access check takes place.
  • AccessCheckByType only returns a single granted access value, if we have a lot of object types to test it'd be quick expensive to call 100s if not 1000s of times for a single security descriptor.
While you could solve the first problem by having sufficient local privileges to manually create an access token and the second by using an API which returns a list of granted access such as AccessCheckByTypeResultList there's an "simpler" solution. You can use the Authz APIs, these allow you to manually build a security context with any groups you like without needing to create an access token and the AuthzAccessCheck API supports returning a list of granted access for each object in the type list. It just so happens that this API is the one used by the AD LDAP server itself.

Therefore to perform a "correct" maximum access check you need to do the following steps.
  1. Enumerate the user's group list for the DC from the AD. Local group assignments are stored in the directory's CN=Builtin container.
  2. Build an Authz security context with the group list.
  3. Read a directory object's security descriptor.
  4. Read the object's schema class and build a list of specific schema objects to check:
  • All attributes from the class and its super, auxiliary and dynamic auxiliary classes.
  • All allowable child object classes
  • All assignable control, write-validated and property set extended rights.
  • Convert the gathered schema information into the object type list for the access check.
  • Run the access check and handled the results.
  • Repeat from 3 for every object you want to check.
  • Trust me when I say this process is actually easier said than done. There's many nuances that just produce surprising results, I guess this is why most tooling just doesn't bother. Also my code includes a fair amount of knowledge gathered from reverse engineering the real implementation, but I'm sure I could have missed something.

    Using Get-AccessibleDsObject and Interpreting the Results

    Let's finally get to using the PowerShell command which is the real purpose of this blog post. For a simple check run the following command. This can take a while on the first run to gather information about the domain and the user.

    PS> Get-AccessibleDsObject -NamingContext Default
    Name   ObjectClass UserName       Modifiable Controllable
    ----   ----------- --------       ---------- ------------
    domain domainDNS   DOMAIN\alice   False      True

    This uses the NamingContext property to specify what object to check. The property allows you to easily specify the three main directories, Default, Configuration and Schema. You can also use the DistinguishedName property to specify an explicit DN. Also the Domain property is used to specify the domain for the LDAP server if you don't want to inspect the current user's domain. You can also specify the Recurse property to recursively enumerate objects, in this case we just access check the root object.

    The access check defaults to using the current user's groups, based on what they would be on the DC. This is obviously important, especially if the current user is a local administrator as they wouldn't be guaranteed to have administrator rights on the DC. You can specify different users to check either by SID using the UserSid property, or names using the UserName property. These properties can take multiple values which will run multiple checks against the list of enumerated objects. For example to check using the domain administrator you could do the following:

    PS> Get-AccessibleDsObject -NamingContext Default -UserName DOMAIN\Administrator
    Name   ObjectClass UserName             Modifiable Controllable
    ----   ----------- --------             ---------- ------------
    domain domainDNS   DOMAIN\Administrator True       True

    The basic table format for the access check results shows give columns, the common name of the object, it's schema class, the user that was checked and whether the access check resulted in any modifiable or controllable access being granted. Modifiable is things like being able to write attributes or create/delete child objects. Controllable indicates one or more controllable extended right was granted to the user, such as allowing the user's password to be changed.

    As this is PowerShell the access check result is an object with many properties. The following properties are probably the ones of most interest when determining what access is granted to the user.
    • GrantedAccess - The granted access when only specifying the object's schema class during the check. If an access is granted at this level it'd apply to all values of that type, for example if WriteProp is granted then any attribute in the object can be written by the user.
    • WritableAttributes - The list of attributes a user can modify.
    • WritablePropertySets - The list of writable property sets a user can modify. Note that this is more for information purposes, the modifiable attributes will also be in the WritableAttributes property which is going to be easier to inspect.
    • GrantedControl - The list of control extended rights granted to a user.
    • GrantedWriteValidated - The list of write validated extended rights granted to a user.
    • CreateableClasses - The list of child object classes that can be created.
    • DeletableClasses - The list of child object classes that can be deleted.
    • DistinguishedName - The full DN of the object.
    • SecurityDescriptor - The security descriptor used for the check.
    • TokenInfo - The user's information used in the check, such as the list of groups.
    The command should be pretty easy to use. That said it does come with a few caveats. First you can only use the command with direct access to the AD using a domain account. Technically there's no reason you couldn't implement a gatherer like Bloodhound and doing the access check offline, but I just don't. I've not tested it in weirder setups such as complex domain hierarchies or RODCs.

    If you're using a low-privileged user there's likely to be AD objects that you can't enumerate or read the security descriptor from. This means the results are going to depend on the user you use to enumerate with. The best results would be using a domain/enterprise administrator will full access to everything.

    Based on my testing when I've found an access being granted to a user that seems to be real, however it's possible I'm not always 100% correct or that I'm missing accesses. Also it's worth noting that just having access doesn't mean there's not some extra checking done by the LDAP server. For example there's an explicit block on creating Group Managed Service Accounts in Computer objects, even though that will seem to be a granted child object.

    Sudo On Windows a Quick Rundown

    By: tiraniddo
    9 February 2024 at 09:10

    Background

    The Windows Insider Preview build 26052 just shipped with a sudo command, I thought I'd just take a quick peek to see what it does and how it does it. This is only a short write up of my findings, I think this code is probably still in early stages so I wouldn't want it to be treated too harshly. You can see the official announcement here.


    To run a command using sudo you can just type:


    C:\> sudo powershell.exe


    The first thing to note, if you know anything about the security model of Windows (maybe buy my book, hint hint), is that there's no equivalent to SUID binaries. The only way to run a process with a higher privilege level is to get an existing higher privileged process to start it for you or you have sufficient permissions yourself though say SeImpersonatePrivilege or SeAssignPrimaryToken privilege and have an access token for a more privileged user. Since Vista, the main way of facilitating running more privileged code as a normal user is to use UAC. Therefore this is how sudo is doing it under the hood, it’s just spawning a process via UAC using the ShellExecute runas verb.


    This is slightly disappointing as I was hoping the developers would have implemented a sudo service running at a higher privilege level to mediate access. Instead this is really just a fancy executable that you can elevate using the existing UAC mechanisms. 


    The other sad thing is, as is Microsoft tradition, this is a sudo command in name only. It doesn’t support any policies which would allow a user to run specific commands elevated, either with a password requirement or without. It’ll just run anything you give it, and only if that user can pass a UAC elevation prompt.


    There are four modes of operation that can be configured in system settings, why this needs to be a system setting I don’t really know. 


    Initially sudo is disabled, running the sudo command just prints “Sudo is disabled on this machine. To enable it, go to the Developer Settings page in the Settings app”. This isn’t because of some fundamental limit on the behavior of the sudo implementation, instead it’s just an Enabled value in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Sudo which is set to 0.


    The next option (value 1) is to run the command in a new window. All this does is pass the command line you gave to sudo to ShellExecute with the runas verb. Therefore you just get the normal UAC dialog showing for that command. Considering the general move to using PowerShell for everything you can already do this easily enough with the command:


    PS> Start-Process -Verb runas powershell.exe


    The third and fourth options (value 2 and 3) are “With input disabled” and “Inline”. They’re more or less the same, they can run the command and attach it to the current console window by sharing the standard handles across to the new process. They use the same implementation behind the scenes to do this, a copy of the sudo binary is elevated with the command line and the calling PID of the non-elevated sudo. E.g. it might try and running the following command via UAC:


    C:\> sudo elevate -p 1234 powershell.exe


    Oddly, as we’ll see passing the PID and the command seems to be mostly unnecessary. At best it’s useful if you want to show more information about the command in the UAC dialog, but again as we’ll see this isn’t that useful.


    The only difference between the two is “With input disabled” you can only output text from the elevated application, you can’t interact with it. Whereas the Inline mode allows you to run the command elevated in the same console session. This final mode has the obvious risk that the command is running elevated but attached to a low privileged window. Malicious code could inject keystrokes into that console window to control the privileged process. This was pointed out in the Microsoft blog post linked earlier. However, the blog does say that running it with input disabled mitigates this issue somewhat, as we’ll see it does not.

    How It Really Works

    For the “New Window” mode all sudo is doing is acting as a wrapper to call ShellExecute. For the inline modes it requires a bit more work. Again go back and read the Microsoft blog post, tbh it gives a reasonable overview of how it works. In the blog it has the following diagram, which I’ll reproduce here in case the link dies.


    A diagram showing how sudo on windows works. Importantly it shows that there's an RPC channel between a normal sudo process and an elevated one.


    What always gets me interested is where there’s an RPC channel involved. The reason a communications channel exists is due to the limitations of UAC, it very intentionally doesn’t allow you to attach elevated console processes to an existing low privileged console (grumble UAC is not a security boundary, but then why did this do this if it wasn’t grumble). It also doesn’t pass along a few important settings such as the current directory or the environment which would be useful features to have in a sudo like command. Therefore to do all that it makes sense for the normal privileged sudo to pass that information to the elevated version.


    Let’s check out the RPC server using NtObjectManager:


    PS> $rpc = Get-RpcServer C:\windows\system32\sudo.exe

    PS> Format-RpcServer $rpc

    [

      uuid(F691B703-F681-47DC-AFCD-034B2FAAB911),

      version(1.0)

    ]

    interface intf_f691b703_f681_47dc_afcd_034b2faab911 {

        int server_PrepareFileHandle([in] handle_t _hProcHandle, [in] int p0, [in, system_handle(sh_file)] HANDLE p1);

        int server_PreparePipeHandle([in] handle_t _hProcHandle, [in] int p0, [in, system_handle(sh_pipe)] HANDLE p1);

        int server_DoElevationRequest([in] handle_t _hProcHandle, [in, system_handle(sh_process)] HANDLE p0, [in] int p1, [in, string] char* p2, [in, size_is(p4)] byte* p3[], [in] int p4, [in, string] char* p5, [in] int p6, [in] int p7, [in, size_is(p9)] byte* p8[], [in] int p9);

        void server_Shutdown([in] handle_t _hProcHandle);

    }


    Of the four functions, the key one is server_DoElevationRequest. This is what actually does the elevation. Doing a quick bit of analysis it seems the parameters correspond to the following:


    HANDLE p0 - Handle to the calling process.

    int p1 - The type of the new process, 2 being input disabled, 3 being inline.

    char* p2 - The command line to execute (oddly, in ANSI characters)

    byte* p3[] - Not sure.

    int p4 - Size of p3.

    char* p5 - The current directory.

    int p6 - Not sure, seems to be set to 1 when called.

    int p7 - Not sure, seems to be set to 0 when called.

    byte* p8 - Pointer to the environment block to use.

    int p9 - Length of environment block.


    The RPC server is registered to use ncalrpc with the port name being sudo_elevate_PID where PID is just the value passed on the elevation command line for the -p argument. The PID isn’t used for determining the console to attach to, this is instead passed through the HANDLE parameter, and is only used to query its PID to pass to the AttachConsole API.


    Also as said before as far as I can tell the command line you want to execute which is also passed to the elevated sudo is unused, it’s in fact this RPC call which is responsible for executing the command properly. This results in something interesting. The elevated copy of sudo doesn’t exit once the new process has started, it in fact keeps the RPC server open and will accept other requests for new processes to attach to. For example you can do the following to get a running elevated sudo instance to attach an elevated command prompt to the current PowerShell console:


    PS> $c = Get-RpcClient $rpc

    PS> Connect-RpcClient $c -EndpointPath sudo_elevate_4652

    PS> $c.server_DoElevationRequest((Get-NtProcess -ProcessId $pid), 3, "cmd.exe", @(), 0, "C:\", 1, 0, @(), 0)


    There are no checks for the caller’s PID to make sure it’s really the non-elevated sudo making the request. As long as the RPC server is running you can make the call. Finding the ALPC port is easy enough, you can just enumerate all the ALPC ports in \RPC Control to find them. 


    A further interesting thing to note is that the type parameter (p1) doesn’t have to match the configured sudo mode in settings. Passing 2 to the parameter runs the command with input disabled, but passing any other value runs in the inline mode. Therefore even if sudo is configured in new window mode, there’s nothing stopping you running the elevated sudo manually, with a trusted Microsoft signed binary UAC prompt and then attaching the inline mode via the RPC service. E.g. you can run sudo using the following PowerShell:


    PS> Start-Process -Verb runas -FilePath sudo -ArgumentList "elevate", "-p", 1111, "cmd.exe"


    Fortunately sudo will exit immediately if it’s configured in disabled mode, so as long as you don’t change the defaults it’s fine I guess.


    I find it odd that Microsoft would rely on UAC when UAC is supposed to be going away. Even more so that this command could have just been a PowerToy as other than the settings UI changes it really doesn’t need any integration with the OS to function. And in fact I’d argue that it doesn’t need those settings either. At any rate, this is no more a security risk than UAC already is, or is it…


    Looking back at how the RPC server is registered can be enlightening:


    RPC_STATUS StartRpcServer(RPC_CSTR Endpoint) {

      RPC_STATUS result;


      result = RpcServerUseProtseqEpA("ncalrpc", 

          RPC_C_PROTSEQ_MAX_REQS_DEFAULT, Endpoint, NULL);

      if ( !result )

      {

        result = RpcServerRegisterIf(server_sudo_rpc_ServerIfHandle, NULL, NULL);

        if ( !result )

          return RpcServerListen(1, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, 0);

      }

      return result;

    }


    Oh no, that’s not good. The code doesn’t provide a security descriptor for the ALPC port and it calls RpcServerRegisterIf to register the server, which should basically never be used. This old function doesn’t allow you to specify a security descriptor or a security callback. What this means is that any user on the same system can connect to this service and execute sudo commands. We can double check using some PowerShell:


    PS> $as = Get-NtAlpcServer

    PS> $sudo = $as | ? Name -Match sudo

    PS> $sudo.Name

    sudo_elevate_4652

    PS> Format-NtSecurityDescriptor $sudo -Summary

    <Owner> : BUILTIN\Administrators

    <Group> : DESKTOP-9CF6144\None

    <DACL>

    Everyone: (Allowed)(None)(Connect|Delete|ReadControl)

    NT AUTHORITY\RESTRICTED: (Allowed)(None)(Connect|Delete|ReadControl)

    BUILTIN\Administrators: (Allowed)(None)(Full Access)

    BUILTIN\Administrators: (Allowed)(None)(Full Access)


    Yup, the DACL for the ALPC port has the Everyone group. It would even allow restricted tokens with the RESTRICTED SID set such as the Chromium GPU processes to access the server. This is pretty poor security engineering and you wonder how this got approved to ship in such a prominent form. 


    The worst case scenario is if an admin uses this command on a shared server, such as a terminal server then any other user on the system could get their administrator access. Oh well, such is life…


    I will give Microsoft props though for writing the code in Rust, at least most of it. Of course it turns out that the likelihood that it would have had any useful memory corruption flaws to be low even if they'd written it in ANSI C. This is a good lesson on why just writing in Rust isn't going to save you if you end up just introducing logical bugs instead.


    ❌
    ❌