Skip to content

Instantly share code, notes, and snippets.

@mdowst
Last active January 7, 2022 16:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdowst/d0f331593be116626205476f1df63fe3 to your computer and use it in GitHub Desktop.
Save mdowst/d0f331593be116626205476f1df63fe3 to your computer and use it in GitHub Desktop.
<#
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