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