# Deep Dive into ATEAM and Azure Resource Attribution

### Introduction

* **NetSPI blog:** [**Here**](https://www.netspi.com/blog/technical-blog/cloud-pentesting/azure-resource-attribution-via-tenant-id-enumeration/)
* **ATEAM tool:** [**Here**](https://github.com/NetSPI/ATEAM/tree/main)

I’ve started really grinding in the cloud security field lately. I felt the need to dive deep and really get into the game. This is my first time doing a research like this. As I started to learn more about azure, I stumbled upon the blog by NetSPI: **We Know What You Did (in Azure) Last Summer**, this blog fully caught my attention, the techniques were so interesting, and I wanted to understand the why and how behind this cloud recon.

All kudos to the [NetSPI](https://x.com/NetSPI) team and especially [Karl Fosaaen](https://x.com/kfosaaen). Their blog posts are so well-done that inspired me to do further research testing and verification.

***

### API Calls via curl

Most of these query can be found in the tool on their [github](https://github.com/NetSPI/ATEAM), but I’ve turned them to curl commands, in case you like to do it manually. I’ve also included two services that weren’t in the tool but were mentioned in the blog: **Azure Resource Manager** and **Machine Learning.** (There's a bonus one)

* **Key vault: WWW-Authenticate**

  ```bash
  curl -I -H "x-ms-version: 2019-12-12" "https://<org-name>.vault.azure.net/keys"
  ```
* **Blob: WWW-Authenticate**

  ```bash
  curl -I -H "x-ms-version: 2019-12-12" "https://<storage_account_name>.blob.core.windows.net/?comp=blobs"
  # fallback query if the first one is not working
  curl -I -X GET -H "x-ms-version: 2019-12-12" "https://<storage_account_name>.blob.core.windows.net/?restype=service&comp=properties" 
  ```
* **Sharepoint: WWW-Authenticate**

  ```bash
  curl -I -H "Authorization: Bearer" -H "Accept: application/json" "https://<org-name>.sharepoint.com/_vti_bin/client.svc"
  ```

  **Sharepoint (default.aspx): Redirect Location**

  ```bash
  curl -I "https://<org-name>.sharepoint.com/_forms/default.aspx"
  ```
* **App service (web): Redirect Location, if 401: WWW-Authenticate**

  ```bash
  curl -I -L -v "https://<app-name>.azurewebsites.net"
  # authenticated
  curl -I -H "Cookie: ESTSAUTHPERSISTENT=<token>" "https://<app-name>.azurewebsites.net"
  ```
* **App service (SCM/Kudu): Redirect Location**

  ```bash
  curl -I "https://<app-name>.scm.azurewebsites.net/"
  # authenticated
  curl -I -H "Cookie: ESTSAUTHPERSISTENT=<token>" "https://<app-name>.scm.azurewebsites.net"
  ```

  > Note: Mind that when dealing with app services without entraID session you might get redirect to the "common" tenant, you can get your `ESTSAUTHPERSISTENT`(the "stay signed in" option is on) use the cookie and attach to the same query, it will return the target tenant ID (since the tool doesn't support creds for now, you gotta do this manually)
* **DataBricks: Redirect Location**

  ```bash
  curl -I "https://<workspace-name>.azuredatabricks.net/aad/auth?hash="
  ```
* **DevOps: WWW-Authenticate, X-VSS-ResourceTenant**

  ```bash
  curl -v -X POST "https://dev.azure.com/<org-name>/_apis/Contribution/HierarchyQuery" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "X-TFS-FedAuthRedirect: Suppress" \
     -H "X-TFS-Session: <org-name>" \
     -d '{"contributionIds": ["ms.vss-features.my-organizations-data-provider"], "dataProviderContext": {"properties": {}}}'
  ```

  **VSSPS: WWW-Authenticate, X-VSS-ResourceTenant**

  ```bash
  curl -k "https://<org-name>.vssps.visualstudio.com/_apis/Token/SessionToken"
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -H "X-TFS-FedAuthRedirect: Suppress" \
    -H "X-TFS-Session: <org-name>"
  ```
* **Resource manager: WWW-Authenticate**

  ```bash
  curl -I "https://management.azure.com/subscriptions/<subID>?api-version=2022-12-01"
  ```

***

### What’s up with the Versioning?

In Azure, you’ll see two main ways of versioning, and they serve different purposes. This was mad confusing when I started to learn it, so let’s break it down.\
Btw both use the ISO 8601 format: `YYYY-MM-DD`

#### Does Header Version Matter?

* **`x-ms-version` header versions:** [**Here**](https://learn.microsoft.com/en-us/rest/api/storageservices/previous-azure-storage-service-versions)

You will see the header **`x-ms-version`** primarily in Blob Storage. You might ask: what's all that about?

So apparently, Microsoft decided to use this way of controlling the version of the APIs cuz most corps won't adapt the newest one immediately. This is the way for them to control dependencies, but still, they encourage people to use the newest version if possible.

So.. the question is: should the header version always be the same (2019-12-12)?\
Not quite, at least in my testing. Key Vault seems to accept most requests regardless of that header, but **Blob Storage usually takes anything newer than 2019-12-12** to trigger the specific authentication challenges we want. If you use a version too old, you might not get that juicy `WWW-Authenticate` header with the Tenant ID.

***

### REST API Query Parameters (api-version)

* **Resource Management 2022-12-01:** [**Here**](https://learn.microsoft.com/en-us/rest/api/resources/operation-groups?view=rest-resources-2022-12-01)
* **Resource Manager:** [**Here**](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/overview)
* **REST API:** [**Here**](https://learn.microsoft.com/en-us/rest/api/azure/#components-of-a-rest-api-requestresponse)

So, why does the Resource Manager put the version in the URL instead of a header? Well, this is just how the Azure Resource Manager (ARM) system is. These APIs handle operations things like creating, updating, or deleting your infrastructure.

Most Azure services have their own specific set of rules, but the base call almost always goes through the same domain: `management.azure.com`

While these calls usually require a valid authentication token to actually do anything, what we’re aiming for is just the **Tenant ID** revealed in the response.

In the official docs, there’s a dropdown box on the left to specify the version. Some versions might have other capabilities, feel free to find the one you want to work with:

<div align="center"><figure><img src="/files/K44dUKKxGukgs7QBwBM0" alt="" width="563"><figcaption></figcaption></figure></div>

NetSPI used the `2022-12-01` version to target Subscriptions, which lets you perform actions like `Get` or `List`. ANY actions asking for subscription ID will work, regardless of the api version.

> Note: This is not limited to one specific version or action. As long as the service supports Resource Manager calls, it will work.

Here, I'll provide some examples:

* Get: [Resource Management: 2022-12-01](https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/get?view=rest-resources-2022-12-01\&tabs=HTTP)

  ```bash
  curl -I "https://management.azure.com/subscriptions/a1ffc958-d2c7-493e-9f1e-125a0477f536?api-version=2022-12-01"
  ```
* List Locations: [Resource Management: 2022-12-01](https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list-locations?view=rest-resources-2022-12-01\&tabs=HTTP)

  ```bash
  curl -I "https://management.azure.com/subscriptions/a1ffc958-d2c7-493e-9f1e-125a0477f536/locations?api-version=2022-12-01"
  ```
* Decompile - bicep: [Resource Management: 2023-11-01](https://learn.microsoft.com/en-us/rest/api/resources/decompile/bicep?view=rest-resources-2023-11-01\&tabs=HTTP)

  ```bash
  curl -I "https://management.azure.com/subscriptions/a1ffc958-d2c7-493e-9f1e-125a0477f536/providers/Microsoft.Resources/decompileBicep?api-version=2023-11-01"
  ```
* Check Domain Availability: [AI Foundry: 2025-06-01](https://learn.microsoft.com/en-us/rest/api/aifoundry/accountmanagement/check-domain-availability/check-domain-availability?view=rest-aifoundry-accountmanagement-2025-06-01\&tabs=HTTP)

  ```bash
  curl -I "https://management.azure.com/subscriptions/a1ffc958-d2c7-493e-9f1e-125a0477f536/providers/Microsoft.CognitiveServices/checkDomainAvailability?api-version=2025-06-01"
  ```

There's more to it, but just give you some examples, the `Get` function of the subscription service is arguably the easiest one to work with.

All of these query will return a `WWW-Authenticate` header:

```bash
HTTP/2 401
...
www-authenticate: Bearer authorization_uri="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", error="invalid_token", error_description="The authentication failed because of missing 'Authorization' header."
...
```

Btw the tenant ID `72f988bf-86f1-41af-91ab-2d7cd011db47` is owned by Microsoft, based on my previous tests on each curl query, or you can try with the graphic explorer: [Here](https://developer.microsoft.com/graph/graph-explorer?request=tenantRelationships%2Fmicrosoft.graph.findTenantInformationByTenantId\(tenantId%3D'72f988bf-86f1-41af-91ab-2d7cd011db47'\)\&method=GET\&version=v1.0\&GraphUrl=https://graph.microsoft.com)

***

### Azure Machine Learning (AML)

* **Compute instance:** [**Here**](https://learn.microsoft.com/en-us/azure/machine-learning/concept-compute-instance?view=azureml-api-2)
* **Azure Private Endpoint private DNS zone values:** [Here](https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-dns)
* **Azure Public region:** [**Here**](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-custom-dns?view=azureml-api-2\&tabs=azure-powershell#azure-public-region)**, Example:** [**Here**](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-custom-dns?view=azureml-api-2\&tabs=azure-powershell#example-hosts-file)

The Machine Learning service was the trickiest part of my research. Honestly, I wasn't fully sure how this attack was even possible at first. NetSPI listed `instances.azureml.ms` in their table, which refers to the **Azure Machine Learning Compute Instance** [Here](https://learn.microsoft.com/en-us/azure/machine-learning/concept-compute-instance?view=azureml-api-2)

So, what is an AML compute instance exactly? Here’s the gist:

> Machine Learning compute instance is a managed cloud-based workstation for data scientists. **Each compute instance** **has only one owner**, although you can share files between multiple compute instances.

In the documentation, Microsoft mentions you can secure your compute instance with  [No public IP](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-secure-training-vnet?view=azureml-api-2). HOWEVER, **this setting is NOT on by default.** That means there are probably a good amount of instances out there still rocking with Public IP.

The public DNS patterns:

* `<compute_instance_name>.<region>.instances.azureml.ms`
* `<compute_instance_name>-22.<region>.instances.azureml.ms`

So a FQDN looks like this: `mycomputeinstance.eastus.instances.azureml.ms`. When I started testing this, the Location header was there, but it kept getting redirection to the "common" tenant, which was a thing NetSPI team mentioned in the App Services.

#### Testing and Validation

My intuition told me to try the same trick: attach the `ESTSAUTHPERSISTENT` cookie to bypass the generic redirect. I tried it, and... nope. It didn't work. It just told me I had no access, and even when it did redirect, it was still that same redirection to "common" tenant.

I figured it might the fact that it's meant for private use, making it hard to find a public instance (also Microsoft said: if it's public then it will cost more). So, I decided to spin up an instance myself. I set everything by default, including the public DNS settings. The results? Still nothing. I also tried many headers and cookies, but I kept hitting the "common" redirect.

While head banging the wall, I quickly realized that NetSPI actually have the slide in their ATEAM github. Huge shout out to Karl, he replied to my X post:

<figure><img src="/files/YGzhWHwIdqlso2Y1E4fM" alt="" width="544"><figcaption></figcaption></figure>

Then I found the slide mentioned AML. They pointed out that the API endpoint (`/api/metrics/v1`) redirects to the "common" tenant. **It turns out the redirect is by design.** WELL IT FREAKING IS. Damn... I should’ve read the slides first:

<figure><img src="/files/3Q1aLvWEmjzvt7Pz3vdS" alt=""><figcaption><p>Page 34, on <a href="https://github.com/NetSPI/ATEAM/blob/main/AzureTenantEnum.pdf">https://github.com/NetSPI/ATEAM/blob/main/AzureTenantEnum.pdf</a></p></figcaption></figure>

Btw one weird thing I noticed: you can name an instance almost anything without restriction. This design probably expect users will use in private, but think about it: if two different users try to use the same name on a public DNS, they would hit a namespace collision. Since these URLs must be globally unique within a region, the `<region>` part of the URL is the only thing preventing such contradiction, **still I'm not too confident about this**, like even name it to "test" or "microsoft" will work!

***

### Azure Advanced Threat Protection..? (APT)

* **Slides:** [**Here**](https://github.com/NetSPI/ATEAM/blob/main/AzureTenantEnum.pdf)
* **MDI:** [**Here**](https://learn.microsoft.com/en-us/defender-for-identity/what-is)
* **MDI portal:** [**Here**](https://learn.microsoft.com/en-us/defender-for-identity/microsoft-365-security-center-mdi)

While reading the slides, I found a service I've never heard before: **Azure Advanced Threat Protection**. After some digging, it's now renamed to **Microsoft Defender for Identity**, the domain seems.. obsolete now, or not? I assume it's technically in use but not that widely, since it mentioned in the slides:

For now, the portal of MDI will be `security.microsoft.com`

I assume what they told you will still work since it's not long ago:

```bash
curl <org>.atp.azure.com

curl <org>sensorapi.atp.azure.com 
```

Apparently, AADInternals will make use of this domain as well: [Here](https://aadinternals.com/aadinternals/#invoke-aadintreconasoutsider)

```powershell
Invoke-AADIntReconAsOutsider -Domain "company.com" | Format-Table

Invoke-AADIntReconAsOutsider -UserName "user@company.com" | Format-Table

Invoke-AADIntReconAsOutsider -Domain "company.com" -GetRelayingParties | Format-Table
```

***

### Reflection

At the every start of this research, I didn't read all the slides, kinda just jumped in and fucked around. It wasn't until the machine learning part that I actually slowed down and read the slide, so you may find some sentences here similar to the slides, especially around the Resource Manager. Well, at least my research direction was right!

To be honest, I never intended to do this research at all. I just truly wanted to learn the "why," not just the "how." I was curious about the stuff that wasn't in the tools or the main blog post. This was my first time ever doing something like this, and the process felt incredibly rewarding. Sometimes you just want to give up when nothing is working, and damn, is this how a security researcher feels..? I'm starting to shift myself into that position, and man, it’s an amazing feeling.

Again, shout out to the NetSPI team, all kudos to them. I'm just a curious guy doing further research on their post for my own learning. **This post is never meant to replace or correct the original findings.** I can't guarantee that all of my research is 100% accurate, there might be a few errors or terms I got confused with, so take it with a grain of salt.&#x20;

Anyway, go check out their [posts](https://www.netspi.com/blog/technical-blog/) if you haven't, try out their tools (I love [MicroBurst](https://github.com/NetSPI/MicroBurst)), and thank them for the incredible findings. Hopefully, I'll do more research like this in the future. Also, if you have any questions or suggestions on my post whether the looks, content, or you just want to roast me. Feel free to DM me on X, I would greatly appreciate you. Thanks for reading til the end. Hope you learned something. Until next time, cya!


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://vix-w1zzer.gitbook.io/vixwizzer/technical-research/deep-dive-into-ateam-and-azure-resource-attribution.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
