Last active
January 7, 2022 16:56
-
-
Save mdowst/d0f331593be116626205476f1df63fe3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
This script will query all Azure Subscriptions for Windows Virtual Machines and check them for the presence of potential Log4j vulnerabilities. | |
It checks for Java installations and search all files for Log4j JAR files. This is not a conclusive scan, but is a good way to quickly | |
identify potentially at risk machines. | |
To function correctly, the VM requires connectivity (port 443) to Azure public IP addresses. | |
Updated 12/22/2021 - Now includes ability to scan Arc servers | |
The account running this script must have read access to the VMs along with | |
Microsoft.Compute/locations/runCommands/read | |
Microsoft.Compute/virtualMachines/runCommand/action | |
The Virtual Machine Contributor role and higher levels have these permission. | |
Requires Modules | |
Az.Accounts | |
Az.Compute | |
Az.ConnectedMachine | |
#> | |
# The tenant to connect to check all subscriptions | |
$Tenant = '' | |
# Or subscription GUID. Tenant ID is not required if subscription is supplies | |
$SubscriptionId = '' | |
$ExcludeVM = $false | |
$ExcludeArc = $false | |
# Set the number of jobs allow to run at once | |
$concurrentJobs = 5 | |
# The Script to execute on the remote system | |
$ScriptContent = { | |
$ProgressPreference = 'SilentlyContinue' | |
$Results = [ordered]@{} | |
$Results.Add('Computer', $env:COMPUTERNAME) | |
# Check if Java.exe is found | |
$javaCmd = Get-Command Java -ErrorAction SilentlyContinue | Select-Object Name, @{l = 'Version'; e = { $_.Version.ToString() } } | |
$Results.Add('JavaCmd', $javaCmd) | |
# Check if Java is is registered in WMI | |
$java = Get-CimInstance -Class Win32_Product -Filter "Name like '%Java%'" | Select-Object IdentifyingNumber, Name, Vendor, Version, Caption | |
$Results.Add('JavaWMI', $java) | |
# Search the entire file system for Log4j files | |
[System.Collections.Generic.List[PSObject]] $Files = @() | |
$drives = Get-Volume | Where-Object { $_.DriveLetter -and $_.FileSystemLabel -ne 'Temporary Storage' -and $_.DriveType -ne 'Removable' } | Sort-Object DriveLetter | Select-Object -ExpandProperty DriveLetter | |
foreach ($d in $drives) { | |
Get-ChildItem -Path "$($d):\" -Filter 'Log4j*.jar' -Recurse -ErrorAction SilentlyContinue | Select-Object Name, FullName | Foreach-Object { $Files.Add($_) } | |
} | |
$Results.Add('Files', $Files) | |
[pscustomobject]$Results | ConvertTo-Json -Compress | |
} | |
# Create a temporary script file | |
$scriptFile = New-TemporaryFile | |
Set-Content -Path $scriptFile -Value $ScriptContent.ToString() | |
$ScriptPath = $scriptFile.FullName | |
Function Invoke-ArcCommand { | |
[CmdletBinding()] | |
param( | |
$ArcSrv, | |
$ScriptContent | |
) | |
$encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent.ToString())) | |
$body = @{ | |
"location" = $ArcSrv.Location | |
"properties" = @{ | |
"publisher" = "Microsoft.Compute" | |
"typeHandlerVersion" = "1.10" | |
"type" = "CustomScriptExtension" | |
"forceUpdateTag" = (Get-Date).ToFileTime() | |
"settings" = @{ | |
"commandToExecute" = "powershell.exe -EncodedCommand $encodedcommand" | |
} | |
} | |
} | |
$URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" | |
$submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) | |
$timer = [system.diagnostics.stopwatch]::StartNew() | |
do { | |
$ext = Get-AzConnectedMachineExtension -ResourceGroupName $ArcSrv.ResourceGroupName -MachineName $ArcSrv.Name | | |
Where-Object { $_.InstanceViewType -eq 'CustomScriptExtension' } | |
} while ($ext.ProvisioningState -notin 'Updating','Creating','Waiting' -and $timer.Elapsed.TotalSeconds -le 30) | |
$timer.Stop() | |
if ($timer.Elapsed.TotalSeconds -gt 30) { | |
"Failed to start the provisioning - $($ext.ProvisioningState)" | |
} | |
elseif ($submit.StatusCode -ne 202) { | |
$submit.Content | |
} | |
else{ | |
$true | |
} | |
} | |
Function Get-vmOutput{ | |
param( | |
$vmJob | |
) | |
$StdOut = [string]::Empty | |
$StdErr = [string]::Empty | |
if($vmJob.type -eq 'Arc'){ | |
if(-not [string]::IsNullOrEmpty($vmJob.vmOutput.InstanceViewStatusMessage)){ | |
$StdOut = $vmJob.vmOutput.InstanceViewStatusMessage | |
} | |
elseif(-not [string]::IsNullOrEmpty($vmJob.vmOutput.StatusMessage)){ | |
$StdOut = $vmJob.vmOutput.StatusMessage | |
} | |
else{ | |
$StdOut = $vmJob.vmOutput | |
} | |
if($StdOut.IndexOf('StdOut:') -gt 0){ | |
$StdOut = $StdOut.Substring($StdOut.IndexOf('StdOut:') + 7) | |
} | |
if($StdOut.IndexOf(', StdErr:') -gt 0){ | |
$StdErr = $StdOut.Substring($StdOut.IndexOf(', StdErr:') + 10) | |
$StdOut = $StdOut.Substring(0, $StdOut.IndexOf(', StdErr:')) | |
} | |
else{ | |
$StdErr = '' | |
} | |
} | |
else{ | |
$StdOut = $vmJob.vmOutput.value | Where-Object{ $_.Code -eq 'ComponentStatus/StdOut/succeeded' } | Select-Object -ExpandProperty Message | |
$StdErr = $vmJob.vmOutput.value | Where-Object{ $_.Code -ne 'ComponentStatus/StdOut/succeeded' } | Select-Object -ExpandProperty Message | |
if([string]::IsNullOrEmpty($StdOut)){ | |
$StdErr = $vmJob.vmOutput | |
} | |
} | |
try{ | |
$StdOutReturn = $StdOut.Trim() | ConvertFrom-Json -ErrorAction Stop | |
} | |
catch{ | |
$StdOutReturn = $StdOut | |
} | |
try{ | |
$StdErrReturn = $StdErr.Trim() | ConvertFrom-Json -ErrorAction Stop | |
} | |
catch{ | |
$StdErrReturn = $StdErr | |
} | |
if($vmJob.State -eq 'Failed'){ | |
$StdErrReturn = $StdOutReturn | |
$StdOutReturn = '' | |
} | |
@{ | |
StdOut = $StdOutReturn | |
StdErr = $StdErrReturn | |
} | |
} | |
[System.Collections.Generic.List[PSObject]] $AllVms = @() | |
$timer = [system.diagnostics.stopwatch]::StartNew() | |
if([string]::IsNullOrEmpty($SubscriptionId)){ | |
# Connect to the tenant | |
if (-not (Get-AzContext | Where-Object { $_.Tenant.Id -eq $Tenant -or $_.Tenant.Name -eq $Tenant })) { | |
Set-AzContext -Tenant $Tenant -ErrorAction SilentlyContinue | Out-Null | |
if (-not (Get-AzContext | Where-Object { $_.Tenant.Id -eq $Tenant -or $_.Tenant.Name -eq $Tenant })) { | |
Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue | |
Connect-AzAccount -Tenant $Tenant | Out-Null | |
} | |
} | |
Write-Host "You are now connected to the Tenant $($(Get-AzContext).Tenant)" | |
# Get all active subscriptions and exclude Azure AD ones | |
$allSubscriptions = Get-AzSubscription | Where-Object { $_.State -eq 'Enabled' -and $_.SubscriptionPolicies.QuotaId -ne 'AAD_2015-09-01' } | |
} | |
else{ | |
if($(Get-AzContext).Subscription.SubscriptionId -ne $SubscriptionId){ | |
Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction SilentlyContinue | Out-Null | |
if($(Get-AzContext).Subscription.SubscriptionId -ne $SubscriptionId){ | |
Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue | |
Add-AzAccount -SubscriptionId $SubscriptionId | Out-Null | |
} | |
} | |
Write-Host "You are now connected to the subscription $($(Get-AzContext).Subscription.Name)" | |
$allSubscriptions = Get-AzSubscription -SubscriptionId $SubscriptionId | |
} | |
foreach ($sub in $allSubscriptions) { | |
[System.Collections.Generic.List[PSObject]] $VMJobs = @() | |
# Set context to subscription | |
if($(Get-AzContext).Subscription.SubscriptionId -ne $sub.Id){ | |
$sub | Set-AzContext | |
} | |
$concurrent = $concurrentJobs - 1 | |
Write-Progress -Activity "Get Machines" -Status "Runtime: $($timer.Elapsed.TotalMinutes.ToString('0')) Minutes" -PercentComplete 0 -id 1 | |
#*************************************************** | |
# Get all AZURE VMs | |
if(-not $ExcludeVM){ | |
Get-AzVM -Status | Where-Object{ $_.Id -notin $AllVms.Id } | Select-Object -Property ResourceGroupName, Name, PowerState, Id, @{l='OsType';e={$_.StorageProfile.OsDisk.OsType}}, JobId, | |
@{l = 'State'; e = { 'Waiting' } }, @{l = 'Type'; e = { 'VM' } }, Location, vmOutput | ForEach-Object { $AllVms.Add($_) } | |
} | |
# Get Hybrid VMs | |
if(-not $ExcludeArc){ | |
Get-AzConnectedMachine | Where-Object{ $_.Id -notin $AllVms.Id } | Select-Object -Property @{l = 'ResourceGroupName'; e = { $_.id.Split('/')[4] } }, Name, | |
@{l = 'PowerState'; e = { $_.Status } }, id, @{l = 'OSType'; e = { $_.OSName } }, JobId, @{l = 'State'; e = { 'Waiting' } }, @{l = 'Type'; e = { 'Arc' } }, | |
Location, vmOutput | FOreach-Object { $AllVms.Add($_) } | |
} | |
# Get Running Windows VMs and Connected Arc Machines | |
$AllVms | Where-Object { $_.PowerState -in 'VM running', 'Connected' -and $_.OSType -eq 'Windows' } | ForEach-Object { $VMJobs.Add($_) } | |
$Count = $VMJobs.Count | |
#*************************************************** | |
# Loop through each VM and run the script | |
for ($i = 0; $i -lt $Count; $i++) { | |
Write-Progress -Activity "Submitted: $(@($AllVms | Where-Object { $_.JobId }).Count) of $($AllVms.Count)" -Status "Processing: $($i) of $Count - Runtime: $($timer.Elapsed.TotalMinutes.ToString('0')) Minutes" -PercentComplete $(($i / $Count) * 100) -id 1 | |
if ($VMJobs[$i].Type -eq 'Arc') { | |
Invoke-ArcCommand -ArcSrv $VMJobs[$i] -ScriptContent $ScriptContent -Verbose | ForEach-Object { | |
if($_ -eq $true){ | |
$VMJobs[$i].State = 'Submitted' | |
$VMJobs[$i].JobId = 'Arc' | |
} | |
else{ | |
$VMJobs[$i].State = 'Failed' | |
$VMJobs[$i].vmOutput = $_ | |
} | |
} | |
} | |
else { | |
# Invoke the script on the Azure VM | |
$AzVMRunCommand = @{ | |
ResourceGroupName = $VMJobs[$i].ResourceGroupName | |
VMName = $VMJobs[$i].Name | |
CommandId = 'RunPowerShellScript' | |
ScriptPath = $ScriptPath | |
AsJob = $true | |
} | |
try{ | |
Invoke-AzVMRunCommand @AzVMRunCommand -ErrorAction Stop | Select-Object Id, State, @{l = 'VM'; e = { $VMJobs[$i].Name } } | | |
ForEach-Object { $VMJobs[$i].JobId = $_.Id; $VMJobs[$i].State = 'Submitted' } | |
} | |
catch{ | |
$VMJobs[$i].State = 'Failed' | |
$VMJobs[$i].vmOutput = $_ | |
} | |
} | |
if ($i -eq $Count - 1 -and $Count -gt 0) { | |
$concurrent = 0 | |
} | |
# Pause when concurrent running count is meet | |
$ActiveStates = 'NotStarted','Running','Submitted','Updating','Creating','Waiting' | |
while (@($VMJobs | Where-Object { $_.State -in $ActiveStates }).Count -gt $concurrent) { | |
Write-Progress -Activity "Submitted: $(@($AllVms | Where-Object { $_.JobId }).Count) of $($AllVms.Count)" -Status "Processing: $($i+1) of $Count - Runtime: $($timer.Elapsed.TotalMinutes.ToString('0')) Minutes" -PercentComplete $(($i / $Count) * 100) -id 1 | |
# Update job states | |
$VMJobs | Where-Object { $_.JobId -and $_.State -notin 'Completed', 'Failed', 'Succeeded' } | ForEach-Object { | |
if ($_.Type -eq 'Arc') { | |
$job = Get-AzConnectedMachineExtension -ResourceGroupName $_.ResourceGroupName -MachineName $_.Name | | |
Where-Object { $_.InstanceViewType -eq 'CustomScriptExtension' } | |
$_.State = $job.ProvisioningState | |
$_.vmOutput = $job | |
} | |
else{ | |
$job = Get-Job -Id $_.JobId -ErrorAction SilentlyContinue | |
$_.State = $job.State | |
} | |
} | |
# Pause for 30 seconds before checking again | |
if (@($VMJobs | Where-Object { $_.State -in $ActiveStates }).Count -gt $concurrent) { | |
$States = @($VMJobs | Group-Object State | Sort-Object Name | FOreach-Object { | |
"$($_.Name) : $($_.Count)" | |
}) -join (' - ') | |
for ($t = 0; $t -lt 30; $t++) { | |
Write-Progress -Activity "$States" -Status "Waiting $(30-$t) seconds" -PercentComplete $(($t / 30) * 100) -id 2 | |
Start-Sleep -Seconds 1 | |
} | |
Write-Progress -Activity "Done" -Id 2 -Completed | |
} | |
} | |
} | |
# Get the output from the VM script executions | |
$AllVms | Where-Object { $_.State -in 'Completed', 'Failed' -and -not $_.vmOutput } | ForEach-Object { | |
try{ | |
$vmOutput = Get-Job $_.JobId | Receive-Job -ErrorAction Stop | |
} | |
catch{ | |
$vmOutput = $_ | |
} | |
$_.vmOutput = $vmOutput | |
} | |
} | |
$timer.Stop() | |
$timer.Elapsed.ToString() | |
# Extract output from script executions | |
$checks = $AllVms | Select-Object -Property *, @{l = 'Msg'; e = { Get-vmOutput -vmJob $_ } } | Select-Object -Property *, @{l = 'Output'; e = { $_.Msg.StdOut } }, | |
@{l = 'ErrorMsg'; e = { $_.Msg.StdErr } } | |
$checks | Select-Object -Property Name, Type, State, @{l='Log4j';e={if($_.Msg.JavaCmd -or $_.Msg.JavaWMI -or $_.Msg.Files){'Found'}elseif(-not $_.Output){'Error'}else{'NotFound'}}}, ErrorMsg | Format-Table | |
$Results = $checks | Select-Object -Property * -ExpandProperty Output -ExcludeProperty Output, JobId, Msg | |
# If Java or Log4j files are found display results here | |
$Results | Where-Object { $_.JavaCmd -or $_.JavaWMI -or $_.Files } | ForEach-Object { | |
Write-Host "`n$($_.Name)" | |
Write-Host ('-' * $_.Name.Length) | |
@($_.JavaCmd) + @($_.JavaWMI) | Where-Object { $_.Name } | ForEach-Object { | |
Write-Host "$($_.Name) - $($_.Version)" | |
} | |
$_.Files | ForEach-Object { | |
Write-Host "$($_.Name) - $($_.FullName)" | |
} | |
Write-Host "" | |
} | |
# Delete the temporary script file | |
if (Test-Path $scriptFile.FullName) { | |
Remove-Item -LiteralPath $scriptFile.FullName -Force | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment