Eigentlich sollte die Seite gar nicht online gehen. Zumindest jetzt noch nicht.
Aufgrund des Log4j Exploits (oder Log4shell, wie ihr wollt) habe ich mich aber dazu entschlossen, zumindest diesen Beitrag zu verfassen. Man möge mir also das Default-Template verzeihen und alles weitere, was noch gemacht werden muss.
Letztlich möchte ich hier nur eine kleine PowerShell-Lösung anbieten, die bei der Analyse zu log4j behilflich sein kann. Mitunter ist ja in Netzwerken nicht bekannt, welche Systeme nun wirklich betroffen sind, oder überhaupt log4j einsetzen.
Das Script von mir möchte ich allerdings ausdrücklich aus ungetestet anbieten!
Es sollte auch sichergestellt werden, dass verlässliche Backups existieren! Ein Probelauf bei mir hat zwar wunderbar funktioniert - aber das muss nicht zwangsweise für andere Systeme sprechen.
Es sollte nur von erfahrenen Administratoren verwendet werden. Es ist auch sicherlich gut, vorher noch einmal einen Blick darauf zu werfen.
Auch sollte man Bewerten, ob man wirklich die "automatische" Bereinigung verwenden möchte, - oder ob man das dann nicht lieber manuell anhand der Ergebnisse macht.
Was macht das Powershell Script nun genau?
Letztlich durchläuft es einen ganzen Netzwerkbereich (muss aktuell aber ein /24 Netz sein) und überprüft, ob dahinter ein System erreichbar ist.
Sofern es ein Windows-System ist, werden dann über WMI die Laufwerke abgefragt.
Diese Laufwerke werden dann einzeln nach log4j*-Dateien durchsucht. Wenn etwas gefunden wurde, wird das .jar-Archiv in ein temporäres Verzeichnis entpackt und aus der Manifest-Datei die Version ausgelesen.
Und sollte hierbei herauskommen, dass es eine Version ab 2.0 ist - so wird in dem .jar-Archiv nach der Jdni-Klasse gesucht.
Fällt auch diese Suche positiv aus, so erfolgt die Rückmeldung direkt in die Powershell, als auch in eine Log-Datei.
Zusätzlich besteht noch die Möglichkeit, die Datei, bzw. Klasse automatisch löschen (was ja eine der vorgeschlagenen Mitigationen ist). Die .jar-Datei wird angeschließend neu gepackt und die Originaldatei ersetzt.
Die Datei führt ihr von einem System aus, das mindestens PowerShell v5.0 installiert hat. Aber das sollte heutzutage ja die Regel sein. Das System sollte Zugriff auf SMB und WMI der Remote-Systeme haben. - Und letztlich sollte es auch unter einem geeigneten Account (z.B. Mitglied der Domänen-Admins) laufen.
Damit das nun funktioniert, sollte man ein eigenes Verzeichnis erstellen, bspw. C:\log4j - es wird im Zweifelsfall vom Script aber auch selbst erstellt.
Dort kann man dann auch gleich seine log4j-scanner.ps1 (oder wie ihr sie nennen wollt) ablegen.
Im letzten Schritt müsst ihr noch die Datei bearbeiten:
Unter dem Dateikopf findet sich der Eintrag:
$ipnet = "192.168.1" - diesen müsst ihr anpassen. Wenn ihr z.B. "192.168.200.0/24" scannen wollt, dann tragt ihr dort "192.168.200" ein - ohne das letzte Oktett.
Ebenfalls solltet ihr $workdir anpassen, sofern ihr nicht meinem Vorschlag folgt und C:\log4j verwendet. Die Datei ist aber hoffentlich auch ausreichend kommentiert.
Und zuletzt empfehle ich, die Powershell mit lokalen Admin-Rechten laufen zu lassen. Es kann sonst bei kopierten .jar-Dateien zu dem Permission-Problem kommen, dass die temporäre Datei nicht mehr gelöscht werden kann.
Aber wie gesagt: Die Nutzung erfolgt ausschließlich auf eigene Gefahr! Ich übernehme keine Verantwortung!
Update 16.12.2021:
- Fehler behoben, der falsche Version als "sicher" identifiziert hat
Update 15.12.2021:
- Es wird nun Version 2.16.0 als "sicher" angezeigt
- das Arbeitsverzeichnis wird nun selbständig erstellt
- ebenfalls wird nun das temporäre Arbeitsverzeichnis mit einem zufälligen Kürzel versehen, falls man simultan mehrere Instanzen des Scripts (bspw. in verschiedenen Netzen) laufen lassen möchte
- rudimentäre Fehlerbehandlung bei der wmic-Anfrage. So spammen Systeme ohne funktionierendes WMIC (oder Nicht-Windows-Systeme) nicht die Konsole voll
- ebenfalls konnte es zu NTFS-Permission-Problemen mit der temporären zip-Datei kommen, das sollte nun in Teilen gefixt sein, sofern die PowerShell mit lokalen Admin-Rechten läuft
Hier nun das Script - einmal direkt auf der Seite und natürlich auch als Download.
# Simple Log4j affected version scanner
#
# --- UNTESTED! USE AT OWN RISK! ---
#
# Description: Runs through network segment (/24) and scans Windows Servers using the admin shares (C$, D$, ...) (if enabled)
# Uses WMIC to get a list of all physical drives.
# .jar files will be unpacked and searched for the exploitable class
# if $fix is $true, it will delete the class, re-pack the .jar and replace the original one
#
# Basic Usage:
# - run this script with local admin rights
# - the user itself should have approriate access to the remote systems (e.g. domain admin)
# - the script can be placed anywhere, the working directory (default: c:\log4j) will be created automatically
# - have a look at the section "Adjustable Variables" to set the IP address to scan, as well as the working directoy and log file
# - to have log4j exploit class removed from the .jar file, set $fix to $true in the settings below (USE AT OWN RISK)
#
# PRECAUTIONS:
#
# When you keep "$fix" at the default value "$false", this script shouldn't do any harm to remote systems, but nevertheless:
# !!! PLEASE ENSURE ON YOUR OWN THAT THIS WILL NOT BREAK YOUR APPLICATION !!!
# !!! PLEASE ENSURE THAT YOU HAVE BACKUPS OR SNAPSHOTS YOU CAN RELY ON !!!
# !!! DON'T BLAME THE AUTHOR IF IT BREAKS YOUR SYSTEM !!!
#
# Copyright (c) 2021 by Hellstorm.De
#
#
# Changelog
# =================================================================
#
# v1.2 - 2021-12-15
# -------------------
# - fix for missing permission when removing copied archive
# - basic error handling for failed WMIC query
# - script now detects fixed version 2.16.0 and marks as "safe"
#
# v1.1 - 2021-12-15
# -----------------
# - using randomized temp directory for running script multiple times (e.g. on different networks)
#
# ===== Adjustable Variables =====
# Network (must be /24) to scan, no trailing ".0" or similar:
$ipnet = "192.168.1"
# network range, normally we want to skip x.x.x.1 (gateway) and x.x.x.255 (broadcast address)
$netstart = 2
$netend = 254
# where to store working files
$workdir = "C:\log4j"
# apply mitigation by removing the affected class from .jar file?
$fix = $false
#generate random temp directory
$randstr = ''
Get-Random -InputObject @('a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z') -Count 16 | % {$randstr+=$_}
$temp = "temp-$($randstr)"
# create working directory if not exist
New-Item -Path $workdir -ItemType "directory" -ErrorAction SilentlyContinue > $null
# create temp dir
New-Item -Path $workdir -Name $temp -ItemType "directory" > $null
# working files/dir, can be, but shouldn't be changed
$inzip = "$($workdir)\$($temp)\tmp.zip"
$outzip = "$($workdir)\$($temp)\new.zip"
$unpdir = "$($workdir)\$($temp)\unpacked"
# logging file, normally stored in workdir
$logfile = "$($workdir)\results-$($randstr).txt"
Write-Host -Fore Cyan "Temporary directory: $($workdir)\$($temp)"
Write-Host -Fore Cyan "Log file: $($logfile)"
# run through the network, skip Gateway .1 and broadcast .255
for($i=$netstart;$i -le $netend; $i++) {
$full = $ipnet+"."+$i
# is the host up?
if($(Test-Connection -ComputerName $full -count 1 -Delay 1 -ErrorAction SilentlyContinue)) {
Write-Host "Scanning $($full)..."
$isclean = $true
$haveaccess = $true
# get the list of physical disks with WMIC; more relieable than Get-PSDDrive
$gwmi = gwmi Win32_LogicalDisk -ComputerName $full -Filter DriveType=3
if(-not $?) {
Write-Host "No WMI access!"
$haveaccess = $false
"NO WMI ACCESS: $full" | Out-File -Append -FilePath $logfile
} else {
$gwmi | Select-Object DeviceID | % {
$netdrive = $_.DeviceID -replace ":","$"
Write-Host "Scanning drive $($_.DeviceID)..."
# Search all local drives for log4j* files
Get-ChildItem -Path "\\$($full)\$($netdrive)" -Filter 'log4j*.jar' -Recurse -File -ErrorAction SilentlyContinue | % {
$isclean = $false
$_.FullName
$file = $_.FullName
# if .jar is found, copy to temp directiry
Copy-Item -Path $file -Destination $inzip
# ... and unzip
Expand-Archive -Path $inzip -DestinationPath $unpdir
# Get version from Manifest file
Get-Content -Path "$($unpdir)\META-INF\MANIFEST.MF" | % {
if($_ -match "Implementation-Version") {
$ver = $_ -replace "^.*: ",""
}
}
# split version string into separate numbers
$vertok = $ver -split "\."
$unsafe = $true
if(($vertok[0].ToInt32($null) -eq 2) -and ($vertok[1].ToInt32($null) -le 15)) {
Write-Host -Fore Red "UNSAFE Version: $($ver)"
} else {
Write-Host -Fore Gree "Safe Version: $($ver)"
$unsafe = $false
}
# if we found a potentially risky version (Major version = 2)
if($unsafe) {
# look for JndiLookup class and notify user/logfile
Get-ChildItem -Path $unpdir -Filter "JndiLookup.class" -Recurse -ErrorAction SilentlyContinue | % {
Write-Host -Fore Red "POTENTIAL EXPLOIT: $($_.FullName)"
"AFFECTED: $($file)" | Out-File -Append -FilePath $logfile
}
# delete JndiLookup class if $fix is $true
if($fix) {
Get-ChildItem -Path $unpdir -Filter "JndiLookup.class" -Recurse -ErrorAction SilentlyContinue | % {
Write-Host "Removing $($_.FullName)..."
Remove-Item -Path $($_.FullName)
"REMOVED: $($_.FullName)" | Out-File -Append -FilePath $logfile
}
# write new .jar file
Compress-Archive -Path "$($unpdir)\*" -DestinationPath $outzip
# and copy back to original location replacing the old file
Copy-Item -Path $outzip -Destination $file -Force
# cleanup
Remove-Item $outzip
}
}
# further cleanup
Remove-Item $inzip -Force
Remove-Item -Recurse -Path $unpdir
}
}
# no findings? Also log. Please note, that this is no warranty for a really unaffected system!
if($isclean -and $haveaccess) {
Write-Host "$($full) seems to be clean"
"NO RESULT: $full" | Out-File -Append -FilePath $logfile
}
}
}
}
#final cleanup
Remove-Item -Recurse -Path "$($workdir)\$($temp)"