Back
Featured image of post Better PowerShell based Log4j vulnerabilities scanner

Better PowerShell based Log4j vulnerabilities scanner

Scan with PowerShell for CVE-2021-44228 and/or CVE-2021-45046 effected versions of Log4j with support for JAR archive scanning!

In my initial post, I also published a very simple way for Windows Admins to find effected Windows boxes via PowerShell. But there was no version check, just a simple scan for Log4j.

A friend told me about a script he found and asked me to change it for him to run on a server and not remotely (they do not have remote PowerShell enabled in some zones).

But scanning all the JAR archives (like I did that on Linux) is something he wanted to have on his Windows Servers. It is also something I wanted to implement anyway in native PowerShell, because not all servers will have WSL installed and can use the Linux based commands.

Anyway, the version of hellstorm.de script is a very smart implementation! I started a minor rework to changed/tweaked it a bit and here is what I came up with:

#requires -Version 5.0 -Modules Storage -RunAsAdministrator
<#
   .SYNOPSIS
   Log4j vulnerabilities affected version scanner

   .DESCRIPTION
   Scan for CVE-2021-44228 and/or CVE-2021-45046 effected versions of Log4j

   .PARAMETER AutoFix
   Apply mitigation by removing the affected class from JAR archive file?

   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(S) IF IT BREAKS YOUR SYSTEM!!!

   .PARAMETER WorkDirectory
   Where to store working files.
   Default: 'C:\temp\log4j-vscan'

   .EXAMPLE
   PS C:\> .\Find-Log4jVulnerabilities.ps1

   .LINK
   https://hochwald.net

   .LINK
   https://www.hellstorm.de/index.php/de/4-log4j-exploit-scanner-und-entferner%C3%BC

   .NOTES
   Reworked version of the .\Find-Log4j.ps1 file from hellstorm.de
   It is based on Version 1.2 of Hellstorm.De's great work

   Copyright (c) 2021 by hellstorm.de
#>
[CmdletBinding(ConfirmImpact = 'Low')]
param
(
   [Parameter(ValueFromPipeline,
   ValueFromPipelineByPropertyName)]
   [Alias('Fix')]
   [switch]
   $AutoFix,
   [Parameter(ValueFromPipeline,
   ValueFromPipelineByPropertyName)]
   [ValidateNotNullOrEmpty()]
   [Alias('TempDirectory')]
   [string]
   $WorkDirectory = 'C:\temp\log4j-vscan'
)

begin
{
   # Generate random temp directory
   $RandomString = [IO.Path]::GetRandomFileName()
   $TempDirectory = ('temp-{0}' -f ($RandomString))

   # Create working directory if not exist
   if (-not (Test-Path -Path $WorkDirectory -ErrorAction SilentlyContinue))
   {
      $null = (New-Item -Path $WorkDirectory -ItemType 'directory' -Force -Confirm:$false -ErrorAction SilentlyContinue)
   }

   # Create temp dir
   if (-not (Test-Path -Path (Join-Path -Path $WorkDirectory -ChildPath $TempDirectory) -ErrorAction SilentlyContinue))
   {
      $null = (New-Item -Path $WorkDirectory -Name $TempDirectory -ItemType 'directory' -Force -Confirm:$false -ErrorAction SilentlyContinue)
   }

   # Working files/dir, can be, but shouldn't be changed
   $TeampArchive = ('{0}\{1}\tmp.zip' -f ($WorkDirectory), ($TempDirectory))
   $FiedArchive = ('{0}\{1}\new.zip' -f ($WorkDirectory), ($TempDirectory))
   $UnpackedDirectory = ('{0}\{1}\unpacked' -f ($WorkDirectory), ($TempDirectory))

   # Logging file, normally stored in workdir
   $LogFile = ('{0}\Log4j-Scann-Results-{1}.txt' -f ($WorkDirectory), (Get-Date -Format 'MM-dd-yyyy_HH-mm-ss'))
}

process
{
   # Get all local disk drives
   $AllFixedDisks = (Get-Volume -ErrorAction SilentlyContinue | Where-Object -FilterScript {
         (($_.DriveType -eq 'Fixed') -and ($_.DriveLetter -ne $null))
   })

   foreach ($FixedDisk in $AllFixedDisks.DriveLetter)
   {
      Write-Verbose -Message ('Scanning drive {0}...' -f $FixedDisk)

      # Search all local drives for log4j* files
      Get-ChildItem -Path ('{0}:\' -f $FixedDisk) -Filter 'log4j*.jar' -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object -Process {
         $isclean = $false
         $JARArchiveFile = $_.FullName

         Write-Verbose -Message ('Scann {0}' -f $JARArchiveFile)

         # if a JAR archive is found, copy to temp directiry
         Copy-Item -Path $JARArchiveFile -Destination $TeampArchive

         # Uncompress the JAR archive
         Expand-Archive -Path $TeampArchive -DestinationPath $UnpackedDirectory

         # Get version from Manifest file
         (Get-Content -Path ('{0}\META-INF\MANIFEST.MF' -f ($UnpackedDirectory)) -ErrorAction SilentlyContinue | ForEach-Object -Process {
               if ($_ -match 'Implementation-Version')
               {
                  $ver = $_ -replace '^.*: ', ''
               }
         })

         # Split version string into separate numbers to compare them
         $vertok = $ver -split '\.'

         # Guess it is unsave until we know better
         $unsafe = $true

         # Handle CVE-2021-44228 and CVE-2021-45046
         if (($vertok[0].ToInt32($null) -eq 2) -and ($vertok[1].ToInt32($null) -le 15))
         {
            # CVE-2021-44228
            Write-Verbose -Message ('Potential CVE-2021-44228 effected Version found: {0}' -f ($ver))
         }
         elseif (($vertok[0].ToInt32($null) -eq 2) -and ($vertok[1].ToInt32($null) -le 16))
         {
            # CVE-2021-45046
            Write-Verbose -Message ('Potential CVE-2021-45046 effected Version found: {0}' -f ($ver))
         }
         elseif ($vertok[0].ToInt32($null) -eq 1)
         {
            # Legacy warning
            Write-Verbose -Message ('Outdated Version: {0}' -f ($ver))
         }
         else
         {
            # Any other version
            Write-Verbose -Message ('Safe Version: {0}' -f ($ver))

            # Skip the next steps
            $unsafe = $false
         }

         # If we found a potentially risky CVE-2021-44228 and/or CVE-2021-45046 version
         if ($unsafe)
         {
            # Look for JndiLookup class and notify user/logfile
            Get-ChildItem -Path $UnpackedDirectory -Filter 'JndiLookup.class' -Recurse -ErrorAction SilentlyContinue | ForEach-Object -Process {
               Write-Verbose -Message ('POTENTIAL EXPLOIT:  Found in {0}' -f $_.FullName)

               ('POTENTIAL AFFECTED: {0}' -f ($JARArchiveFile)) | Out-File -Append -FilePath $LogFile

               Write-Verbose -Message 'You should download Log4j 2.17.0 (or later): https://logging.apache.org/log4j/2.x/download.html'
            }

            # Delete JndiLookup class if $fix is $true
            if ($AutoFix)
            {
               Get-ChildItem -Path $UnpackedDirectory -Filter 'JndiLookup.class' -Recurse -ErrorAction SilentlyContinue | ForEach-Object -Process {
                  Write-Verbose -Message ('Removing {0}...' -f $_.FullName)

                  $null = (Remove-Item -Path $($_.FullName) -Force -Confirm:$false -ErrorAction SilentlyContinue)

                  ('REMOVED: {0}' -f $_.FullName) | Out-File -Append -FilePath $LogFile
               }

               # Write new JAR archive file
               (Compress-Archive -Path ('{0}\*' -f ($UnpackedDirectory)) -DestinationPath $FiedArchive -Force -Confirm:$false -ErrorAction SilentlyContinue)

               # Restore the new JAR archive
               (Copy-Item -Path $FiedArchive -Destination $JARArchiveFile -Force -Confirm:$false)

               # cleanup
               $null = (Remove-Item -Path $FiedArchive -Force -Confirm:$false -ErrorAction SilentlyContinue)
            }
         }

         # Further cleanup
         $null = (Remove-Item -Path $TeampArchive -Force -Confirm:$false -ErrorAction SilentlyContinue)
         $null = (Remove-Item -Recurse -Path $UnpackedDirectory -Force -Confirm:$false -ErrorAction SilentlyContinue)
      }
   }
}

end
{
   # Final Cleanup
   $null = (Remove-Item -Recurse -Path ('{0}\{1}' -f ($WorkDirectory), ($TempDirectory)) -Force -Confirm:$false -ErrorAction SilentlyContinue)
}

The Log might contain something like this:

POTENTIAL AFFECTED: C:\Users\josh\Downloads\apache-log4j-2.11.0-bin\log4j-core-2.11.0.jar

I guess you will spot the differences between the initial script from hellstorm.de and my approach.

There is still room for improvements, and I leave that up to someone else…

Some idea’s:

  • Log if cleaning action was taken
  • More detailed logging in general
  • A hint for outdated versions anyway (e.g. “Please use the the latest version xx.xx”)
  • A real warning if Version 1.x is still in use (Yeah, that is retired, EOL, many moons ago)
  • Dump the Log at the end of each run