Having improved/built on impacket internally at my work place, I wanted to take the next step in contributing to the library by way of new modules. I ended up landing on two: MS-NEGOEX, which remains a work in progress(updates soon) and the focus of this blog, MS-RAA. MS-RAA turned out to be a great first protocol to implement since the spececification is short(less than 50 pages), most of it maps cleanly onto structures within Impacket as well as there being plenty of RPC protocols to look towards and ultimately, the protocol itself answers a question that comes up constantly during an engagement: what can this principal actually access?
I wanted to write this blog to go through the MS-RAA module, its potential usecases, as well issues I experinced in developing the module.
What is MS-RAA?
Lets start off with answering this core question. MS-RAA, also called RAZA (Remote Authorization), is an RPC-based protocol used to perform authorization queries against a remote target. The core idea is simulation: the protocol lets you simulate a access control decision a remote service would make when a given principal tries to touch a resource protected by a given authorization policy.
As per MS-RAA Section 1.3 Overview, the protocol is meant to work around three kinds of “what-if” questions:
- Simulate the groups and/or claims a user would have if they authenticated to a remote service.
- Simulate a user’s access to a hypothetical resource protected by a specific authorization policy.
- Simulate how potential changes to a user’s group or claim assignments would affect access.
Something one may have noticed is here that since these are simulations of access, the users token(what contains the info a system will use to determine access) may differ from if they actually have a full logon. The specification tocuhes on a practical example of this in which Microsoft states a user who logs on with a password is given a different impersonation token than the same user logging on with a smart card, so the groups and claims attached to the resulting context can vary by logon type.
Now moving on to the actual code, MS-RAA interface exposes seven methods:
| Opnum | Method | Purpose |
|---|---|---|
| 0 | AuthzrFreeContext |
Free a client context and its associated state |
| 1 | AuthzrInitializeContextFromSid |
Build a client context from a SID |
| 2 | AuthzrInitializeCompoundContext |
Combine a user context and a device context |
| 3 | AuthzrAccessCheck |
Run a “what-if” access check against security descriptors |
| 4 | AuthzGetInformationFromContext |
Read info (user SID, groups, claims) from a context |
| 5 | AuthzrModifyClaims |
Add/replace/delete claims on a context |
| 6 | AuthzrModifySids |
Add/replace/delete SIDs on a context |
MS-RAA is an RPC based protocol thats on TCP/IP (ncacn_ip_tcp) on a dynamically assigned endpoint, and the server requires Negotiate as the security provider. On the Windows side, the server role ships on Windows Server 2012 and later when configured in a domain environment, and access is through membership in the Windows Authorization Access Group(which by default only Enterprise Domain Controllers are a member of) or you can also query your own context without additional membership.
Weaponizing MS-RAA
There are a few things as one can imagine that could be put to good use with a protocol that allows you to ask “what-if” before needing to make a move. Some of these include:
-
“What can I touch on this box?” without touching it.
AuthzrAccessChecklets you take a principal’s context and a target security descriptor and ask the server to compute the granted access mask. -
Group and claim enumeration from a single SID.
AuthzrInitializeContextFromSidfollowed byAuthzGetInformationFromContextgives you all group SIDs and claims that a principal would carry in their token, all while the server resolves this from Active Directory for you. Additionaly you also get local group membership returned. -
“What if I were in that group?” — privilege-escalation exploration This is the part I find most compelling.
AuthzrModifySidsandAuthzrModifyClaimslet you mutate the simulated context before running an access check. You can add a SID belonging to a group you’re considering adding yourself to, and then re-runAuthzrAccessCheckto see whether that membership would actually unlock access to a target resource. In other words, you can validate an escalation plan you may have come up with, entirely through simulation, negating the need to complete the escalation and attempting to access the target resource. -
Compound contexts for device-aware policies.
AuthzrInitializeCompoundContextlets you merge a user context with a device context, which is relevant when targets use device claims or compound-identity-aware policies. You can model the user-plus-device combination the same way the server would when you are attempting to access it. I believe that this a part of the Authentication Silos Featur set that Microsoft has. This is purely speculative*
A Tour of the Module
Since I used current MS-RPC protocol implementations as my guiding light, MS-RAA does not look too different from LSAT and others: the interface UUID up top, then constants, enumerations, NDR structures, the RPC call/response classes, an OPNUMS table, and finally h-prefixed helper functions that wrap each call into more usefriendly APIs for consumers. Due to the nature of Impacket, only the client side of the protocol is implemented.
The interface UUID and object UUIDs are defined right away:
MSRPC_UUID_RAA = uuidtup_to_bin(('0b1c2170-5732-4e0e-8cd3-d9b16f3b84d7', '0.0'))
RAA_OBJECT_UUID_DEFAULT = '9a81c2bd-a525-471d-a4ed-49907c0b23da'
RAA_OBJECT_UUID_NO_SCOPED_POLICY = '5fc860e0-6f6e-4fc2-83cd-46324f25e90b'
Connecting with the second UUID compared to the first(“default”) tells the server to skip stripping SYSTEM_SCOPED_POLICY_ID_ACEs out of the security descriptor during AuthzrAccessCheck. (More on the object-UUID situation in the final section, because of the issues with Impackets epm implementation)
All of the helper functions take an objectUuid parameter that defaults to RAA_OBJECT_UUID_DEFAULT_BIN, so you can flip to the no-scoped-policy behavior per call as you can simply pass the argument from the caller.
The helper functions
Helpers are the part most people will actually call when consuming to the module. Since Impacket lacks a centralized documentation silo, I have tried to be as indepth as possible with explaianing each of the helper functions avaliable.
hAuthzrInitializeContextFromSid(dce, sid, flags=AUTHZ_COMPUTE_PRIVILEGES, ...): This function servers as the entry point for nearly everything. as it builds an AuthzrInitializeContextFromSid request, and fires the call with the appropriate flags. The function returns the response containing your context handle. By default it sets the AUTHZ_COMPUTE_PRIVILEGES flag so the new context gets its privileges computed.
hAuthzrFreeContext(dce, contextHandle, ...): This function frees the server-side state for a context handle
hAuthzrInitializeCompoundContext(dce, userContextHandle, deviceContextHandle, ...): This function takes two existing context handles (a user context and a device context) and asks the server to build a combined compound context, returning a new handle. This helper is particularly Useful for modeling device-claim-aware policies.
hAuthzrAccessCheck(dce, contextHandle, securityDescriptor, desiredAccess, ...): This helper accepts either a single security descriptor (as a raw octet stream) or a list of them, and builds out the AUTHZR_ACCESS_REQUEST with your desired access mask, an optional PrincipalSelfSid, and an optional object type list. It pre-sizes the reply arrays (GrantedAccessMask and Error) according to resultListLength before sending. The helper will then return the granted access mask(s) and per-result error codes.
hAuthzGetInformationFromContext(dce, contextHandle, infoClass, ...):This helper allows the caller to pass an AUTHZ_CONTEXT_INFORMATION_CLASS value, AuthzContextInfoUserSid, AuthzContextInfoGroupsSids, AuthzContextInfoRestrictedSids, AuthzContextInfoDeviceSids, AuthzContextInfoUserClaims, or AuthzContextInfoDeviceClaims and get back the corresponding slice of the context.
hAuthzrModifyClaims(dce, contextHandle, claimClass, claimOperations, claims=NULL, ...): This helper allows a caller to modify the claims on given a context. It takes a claim class, a list of operations (AUTHZ_SECURITY_ATTRIBUTE_OPERATION values, none/replace-all/add/delete/replace), and the claims to apply. There’s a internl helper here called _enum_operation that lets you pass either a raw integer or an already-built enum instance for each operation. This makes it more flexiable and less likely for people to face issues with the types they are providing.
hAuthzrModifySids(dce, contextHandle, sidClass, sidOperations, sids=NULL, ...): This helper can be seen as the SID counterpart to the claims modifier. They largely have the same shape and set up!
Behind these, the module also implements the raw NDRCALL request/response pairs for each opnum, the enumerations (AUTHZ_CONTEXT_INFORMATION_CLASS, AUTHZ_SECURITY_ATTRIBUTE_OPERATION, AUTHZ_SID_OPERATION), and a DCERPCSessionError that decodes the Win32 error codes the server returns into readable messages.
The test suite in tests/dcerpc/test_raa.py can be consulted for anyone wanting to build an example script for it. Its also super helpful to learn more about how the RPC calls work and their intended functionalities.
Bumps Along the Way
Two problems took up most of my implementation time. I wanted to document both as Im sure that others may potentialy implement a MS-RPC based protocol with the same requirments as MS-RAA.
1. The AUTHZR_TOKEN_GROUPS structure didn’t fit Impacket’s definitions
The way to build a structure in Impacket is to declare a structure tuple and let Impackets NDR implementation handle marshalling and unmarshalling. For most of MS-RAA’s structures that worked, Unfortunately, there was one sneaky little bug that made it very hard!
AUTHZR_TOKEN_GROUPS was a bit of a probelmatic one. This is because that the defined/implemented structure format by Impacket was not accounting for a mismatch in the returned byte stream between an NDR32 and NDR64 ctx.
Lets first insepect the IDL for the call as defined in MS-RAA:
typedef struct _AUTHZR_TOKEN_GROUPS {
DWORD GroupCount;
[size_is(GroupCount)] AUTHZR_SID_AND_ATTRIBUTES Groups[];
} AUTHZR_TOKEN_GROUPS;
The bug occurs when calling AuthzGetInformationFromContext for the groups info class. With a local user SID, the bug never resurfaces however With a domain SID, the NDR decoding of the returned groups triggered the testcases to not pass.
The reason is a difference in how the conformant array’s maximum-count prefix is observed between the two transfer syntaxes: in the NDR64 replies I observed, Windows sends us the conformant-array max count ahead of GroupCount, but in the NDR32 replies it does not include it. Impackets standard structure definition has no way to express “there’s sometimes an extra count here depending on the negotiated syntax,” so the framework’s automatic parsing consumed the wrong bytes and causing downstream shifts.
The fix was to basically create a custom implementation of the structure instead of using Impackets. The AUTHZR_TOKEN_GROUPS structure then had custom fromString/getData (and their *Referents companions) that addressed the issue identified by:
- Checking
self._isNDR64and, only in the NDR64 case, align to 8 bytes and read/write the explicit conformant max-count<Qbefore eatingGroupCount. - Read
GroupCountitself as a plainDWORD. - Use the max count when present (NDR64) and otherwise fall back to
GroupCount(NDR32) to decide how manyAUTHZR_SID_AND_ATTRIBUTESentries to walk. - Manually handle the alignment padding between the count and the array, and between entries.
This was really annoying to deal with as at first I couldnt believe when AI gave me the function when I had asked it about why my implementation wasnt pasing two test cases. After opening a PR and flagging it with the Impacket maintainers, I was suprised to find out that the AI was more right than I was xD.
2. MS-RAA is a non-nil object UUID protocol, and the standard EPM mapper assumes nil
The second problem is about finding the endpoint. MS-RAA uses dynamic endpoints, and so one has to use the Endpoint Mapper (EPM) to find where the interface is listening. Impacket has a epm.hept_map helper to do this for you. It takes in a interface UUID and it returns a binding string that you can then use directly.
This method/approach didnt work due to the difference in how the MS-RAA protocol works.As per MS-RAA Section 3 MS-RAA registers itself in EPM under a non-nil object UUID (the 9a81c2bd-... / 5fc860e0-... numbers/strings from MS-RAA section 2.1. The Impacket helper function breaks in this case because it is effectively hardcodes the object UUID to nil during the lookup, so a hept_map call never matches the MS-RAA registration and comes back empty.
The workaround is to do the more expensive lookup that rpcdump.py performs, which is to enumerate the endpoint mapper’s entries with hept_lookup (matching by interface ID), then walk the returned towers yourself to find the one whose first floor is the MS-RAA interface UUID, dig through its protocol floors for the TCP port identifier, and unpack the related data to get the actual port. In the test setup that looks like:
entries = epm.hept_lookup(
self.machine,
inquiry_type=epm.RPC_C_EP_MATCH_BY_IF,
ifId=raa.MSRPC_UUID_RAA
)
port = None
for entry in entries:
if entry['tower']['Floors'][0]['InterfaceUUID'] != raa.MSRPC_UUID_RAA[:16]:
continue
for floor in entry['tower']['Floors'][3:]:
if 'ProtocolData' in floor.fields and floor['ProtocolData'] == bytes([epm.FLOOR_TCPPORT_IDENTIFIER]):
port = unpack('!H', floor['RelatedData'])[0]
break
if port is not None:
break
Its a bit more invovled and computationally taxing but this is required for all others in the future whom may need to connect to the MS-RAA protocol as we would need to manually do this. Im not sure why impacket doesnt allow you to pass the uuid to the epm.hept_map function but oh well.
Wrapping Up
MS-RAA turned out to be a fun first protocol for me to get merged into Impacket with its small implementation requirments, mostly familiar structures, and a useful/interesting capability for anyone doing authorization enumeration and “what-if” privilege-path analysis on a remote Windows host.
If anyone ends up using the module or extending it in any way then Id love to know! Feel free to reach out : )