Script for detecting potentially vulnerable Log4j jars [CVE-2021-44228] on Windows Server

Update 2021-12-18 – This looks like a much more competent script for detecting this vulnerability and there is a python version for Linux:

Updated 2021-12-17 – Script is v1.4 and looks for .war files now too

Original post below

Inspired by the one-liner here:

gci 'C:\' -rec -force -include *.jar -ea 0 | foreach {select-string "JndiLookup.class" $_} | select -exp Path

I wrote a script to expand on the command, support Windows Server 2008 onward and to be more automated.

This script is basically the one liner with a bit of logic to get all the local fixed disks on a server and iterate through them all looking for Log4j jar file:

    Checks the local system for Log4Shell Vulnerability [CVE-2021-44228]
    Gets a list of all volumes on the server, loops through searching each disk for Log4j stuff
    Using base search from

    Version History
        1.0 - Initial release
        1.1 - Changed ErrorAction to "Continue" instead of stopping the script
        1.2 - Went back to SilentlyContinue, so much noise
        1.3 - Borrowed some improvements from @cedric2bx (
                Replace attribute -Include by -Filter (prevent unauthorized access exception stopping scan)
                Remove duplicate path with Get-Unique cmdlet
        1.4 - Added .war support thanks to @djblazkowicz (
    Created by Eric Schewe 2021-12-13
    Modified by Cedric BARBOTIN 2021-12-14

# Get Windows Version string
$windowsVersion = (Get-WmiObject -class Win32_OperatingSystem).Caption

# Server 2008 (R2)
if ($windowsVersion -like "*2008*") {

    $disks = [System.IO.DriveInfo]::getdrives() | Where-Object {$_.DriveType -eq "Fixed"}

# Everything else
else {

    $disks = Get-Volume | Where-Object {$_.DriveType -eq "Fixed"}


# I have no idea why I had to write it this way and why .Count didn't just work
$diskCount = $disks | Measure-Object | Select-Object Count -ExpandProperty Count

Write-Host -ForegroundColor Green "$(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Starting the search of $($diskCount) disks"

foreach ($disk in $disks) {

    # One liner from
    # gci 'C:\' -rec -force -include *.jar -ea 0 | foreach {select-string "JndiLookup.class" $_} | select -exp Path

    # Server 2008 (R2)
    if ($windowsVersion -like "*2008*") {

        Write-Host -ForegroundColor Yellow "  $(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Checking $($disk.Name): - $($disk.VolumeLabel)"
        Get-ChildItem "$($disk.Name)" -Recurse -Force -Include @("*.jar","*.war") -ErrorAction SilentlyContinue | ForEach-Object { Select-String "JndiLookup.class" $_ } | Select-Object -ExpandProperty Path | Get-Unique

    # Everything else
    else {

        Write-Host -ForegroundColor Yellow "  $(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Checking $($disk.DriveLetter): - $($disk.VolumeLabel)"
        Get-ChildItem "$($disk.DriveLetter):\" -Recurse -Force -Include @("*.jar","*.war") -ErrorAction SilentlyContinue | ForEach-Object { Select-String "JndiLookup.class" $_ } | Select-Object -ExpandProperty Path | Get-Unique



Write-Host -ForegroundColor Green "$(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Done checking all drives"

Sample output with nothing found:

check_CVE-2021-44228.ps1 sample output

Sample output with something found:

check_CVE-2021-44228.ps1 sample output 2

Good luck everyone.

List/Audit all folder delegate permissions on an Exchange mailbox

We recently needed a way to see what delegate permissions a client had given across the vastness that is their mailbox and it’s folder structure.

Digging around online I found this script from John Hopkins which got me 90% of the way there.

Their script was missing three things for my use case:

  • Delegate permissions on the root folder of the mailbox
  • Exclude the actual user from the report
  • Little tidier formatting of the output

This script has been tested against Exchange 2016 only.

# Use displayname
$mailbox = "<DISPLAY NAME>"

$permissions = @()
$folders = Get-Mailboxfolderstatistics $mailbox | % {$_.folderpath} | % {$_.replace(“/”,”\”)}

# Get the root folder of the mailbox
$folderKey = $mailbox + ":" + "\"
$permissions += Get-MailboxFolderPermission -identity $folderKey -ErrorAction SilentlyContinue | Where-Object {$_.User -notlike “Default” -and $_.User -notlike “Anonymous” -and $_.AccessRights -notlike “None” -and $_.User -notlike $mailbox}

# Get the rest of the folders
$list = ForEach ($folder in $folders)
    $folderKey = $mailbox + ":" + $folder
    $permissions += Get-MailboxFolderPermission -identity $folderKey -ErrorAction SilentlyContinue | Where-Object {$_.User -notlike “Default” -and $_.User -notlike “Anonymous” -and $_.AccessRights -notlike “None” -and $_.User -notlike $mailbox}

$permissions | Format-Table -AutoSize



Script to sync Domain Controller SSL Certificates to a specific host

We have an application that uses LDAP over SSL to authenticate users via Active Directory. The server running the application is a member of the domain and has the domains Root CA installed in it’s local certificate store.

Technically the Root CA should be good enough for the server and any applications on it to trust the SSL certificates on our domain controllers because they are signed by that Root CA. Not the case for this application.

We have four Domain Controllers each with a different SSL certificate that expires yearly and each with a different expiry date. Exporting and importing these certificates manually is going to be a huge annoyance.

I wrote a PowerShell script to handle doing it automatically for us. This script is being run against 2012 R2 Domain controllers which is why I use the PowerShell Module exporting the certificates and the target is  2008 R2 which is why the import is handled via ‘certutil’. You could easily swap these out in the script to suite your needs.

# Domain Controller SSL Certificate syncing script
# Created by: Eric Schewe
# Created on: 2018-10-11
# Permission Requirements
# -----------------------------------------------------
# "Remote Management Users" in the domain that holds the DCs you're pulling the certificates from
# Local Admin on the destination server
# Login As Batch Job on what ever server you're running the script from unless you're running it as a local admin on the destination server
# Other requirepments
# -----------------------------------------------------
# A directory called C:\Temp must exist on all servers involved

# The domain we want to get DC certs from
$domainInfo = Get-ADDomain <FQDN OF YOUR DOMAIN>

# Server we're installing the certificates on
$destinationServer = "<FQDN OF THE DESTINATION SERVER>"

# For each Domain Controller in the domain do the following
foreach ($dc in $domainInfo.ReplicaDirectoryServers) {

    # Open a remote session to the DC
    Write-Host "Connecting to $($dc)"
    $sessionCertServer = New-PSSession -ComputerName $dc -EnableNetworkAccess

    # Find the certificates thumbprint that matches the DCs FQDN
    Write-Host "Getting cert list for $($dc)"
    $thumbPrint = Invoke-Command -Session $sessionCertServer -ScriptBlock {(Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -match "$($dc)"}).Thumbprint}

    # Debugging
    Write-Host "Dumping thumb prints from $($dc)"
    Write-Host $thumbPrint

    # Export the certificate on the remote DC, based on it's thumbprint
    Write-Host "Exporting $($thumbPrint) on $($dc)"
    Invoke-Command -Session $sessionCertServer -ScriptBlock {Get-ChildItem -Path Cert:\LocalMachine\My\$thumbPrint | Export-Certificate -FilePath "C:\Temp\$($args[0])-159969.crt" -Type CERT} -argumentlist $dc

    # Copy the exported certificate from the remote server to the local server
    Write-Host "Copying C:\Temp\$($dc)-159969.crt from $dc to C:\Temp locally"
    Copy-Item -FromSession $sessionCertServer -Path "C:\Temp\$($dc)-159969.crt" -Destination "C:\temp\"

    # Remove the exported certificate file on the remote server and close the session
    Write-Host "Cleaning up remote files and sessions"
    Invoke-Command -Session $sessionCertServer -ScriptBlock {Remove-Item -path "C:\Temp\$($args[0])-159969.crt"} -argumentlist $dc
    Remove-PSSession -Session $sessionCertServer


# Open a session to the server we're putting the certs on
$sessionDestinationServer = New-PSSession -ComputerName $destinationServer -EnableNetworkAccess

# Copy each 
foreach ($dc in $domainInfo.ReplicaDirectoryServers) {

    # Import the exported certificate file on the local server and then delete it
    Write-Host "Moving C:\Temp\$($dc)-159969.crt from local server to destination server"
    Copy-Item -ToSession $sessionDestinationServer -Path "C:\Temp\$($dc)-159969.crt" -Destination "C:\temp\"

    Write-Host "Importing certificate for $($dc) into destination server"
    Invoke-Command -Session $sessionDestinationServer -ScriptBlock {certutil -enterprise -f -v -AddStore "Root" "C:\Temp\$($args[0])-159969.crt"} -argumentlist $dc

    Write-Host "Deleting certificate files for $($dc)"
    Remove-Item -path "C:\Temp\$($dc)-159969.crt"
    Invoke-Command -Session $sessionDestinationServer -ScriptBlock {Remove-Item -path "C:\Temp\$($args[0])-159969.crt"} -argumentlist $dc


Remove-PSSession -Session $sessionDestinationServer


Migrating DHCP from 2003 to 2012 R2

This post will likely fall on deaf ears since no one out there is still running Windows Server 2003 right?

Well we are for our oldest DHCP server. Better yet it’s 2003 Standard (non R2) which means I can’t installed Active Directory Management Gateway Service which would allow me to remotely access DHCP on the 2003 server via PowerShell 5.1 running on a different box. Newer versions of PowerShell include support for working with DHCP. PowerShell 2.0 (latest available for Server 2003) does not support these commands.

Googling around found me the standard recommended way of migrating DHCP subnets.

On the source machine run:

netsh dhcp server export

This will export all of the current leases and reservations in the scope into a text file, you can then transfer the text file over to your new DHCP server and run the following to import it:

netsh dhcp server import

The downside to this method is that it causes a temporary outage of your DHCP server during the import/export. I just migrated 80 odd subnets during the day and the outages were so short no one noticed.

Alright so that was the easy part.

In addition to 80 standard DHCP scopes we have 4 superscopes that also need to be migrated.

Attempting to migrate the superscopes using the above method failed. When I attempted to import the scope into the destination DHCP server I got the error “TLS supported but not configured”.

The first post I found for this error in Google links to a Technet discussion where someone states “No, you cannot direct migrate windows server 2003 DHCP/DNS to windows server 2012 DHCP/DNS.”. Clearly not an accurate statement since I’d just migrated 80 standard scopes.

So here I am. I cannot move 4 super scopes using the netsh method, I cannot use PowerShell because the 2003 server is to old and I do not want to upgrade it to 2003 R2 for multiple reasons (did I mention it’s a Domain Controller to?).

What I ended up doing was building a new Windows 2012 R2 Standard box, joined it to our domain so I could remotely access it and have the benefit of domain logins for accessing resources across our network and then installed DHCP on it. Immediately after the DHCP Server installation completed I went into the Windows Firewall and blocked DHCP just in case.

Then on the old DHCP server I ran this command:

netsh dhcp server export all.txt all

I then transferred the “`all.txt“` file over to the DHCP server I just built and ran this:

netsh dhcp server import all.txt all

and ended up with a complete copy of my old DHCP server on my temporary DHCP server including my superscopes, no errors.

Now I can use PowerShell to finish this up. On the temporary DHCP server I ran this:

Export-DhcpServer -ComputerName "localhost" -File "C:\temp\SUPERSCOPE1.xml" -ScopeId, -Leases

transferred “`SUPERSCOPE1.xml“` to the new DHCP server and ran:

Import-DhcpServer -ComputerName "localhost" -File "C:\temp\SUPERSCOPE1.xml" -BackupPath "C:\temp\" -ScopeId, -Leases

And bingo. Superscope successfully migrated from 2003 to 2012R2 with a small middle step.

If you screw up or need to do this in batches over time you can quickly and easily wipe everything out on the temporary DHCP server by doing the following:

  1. Stop the DHCP service
  2. Delete the contents of C:\Windows\system32\dhcp
  3. Start DHCP servoce

You’ll end up with a blank DHCP server that you can re-import a fresh copy of your old DHCP server into.

Powershell script to report on total send/received e-mails in Exchange v2.0

An updated and improved version of my old script from here.

This script has been tested against Exchange 2016 CU4. I do not know if it will work against older versions of Exchange.

The script can be configured to run as a scheduled task and it generates a e-mail report of users who have sent more than ‘x’ e-mails so far today or the previous day.

You can now exclude specific e-mails from the report by placing them in the ‘exceptionList.txt’ file which is created after the scripts first run

We use this script to find compromised accounts that are blasting out spam.

# Users who sent more than 'x' e-mails so far today or yesterday
# The output of this report is then e-mailed
# This script must be called using the argument '-TimeFrame "yesterday"' or '-TimeFrame "today"'
# On first run a file called 'exceptionList.txt' in the same directory as itself if the file doesn't exist. This means the script will need
# read-write access to the location on the first run and read-only access from then on. Once created you can add e-mail addresses into the
# text file (one per line) that you want excluded from the report.
# Note: This script has been specifically tested against Exchange 2016 CU4 and may not work against previous versions of Exchange
# Requirements: The account that runs this script needs at least the "View-Only Organization Management" and the "Records Management" role in Exchange
# Created by: Eric Schewe
# Created on: 2017-08-15

# This command must be run with '-timeFrame today' or '-timeFrame yesterday' specified

# ------------------------- (START) Customize this section per your deployment -------------------------
# Set the minimum sent e-mails count you care about. Anyone who sends more than this number will appear in the report
$minimumEmails = 500

# Location of script, no trailing slash
$scriptLocation = ""

# E-mail stuff
# Multiple e-mail addresses should be in this format "&lt;[email protected]&gt;, &lt;[email protected]&gt;"
$to = ""
$from = "[email protected]"
$smtpServer = ""

# Exchange server you want to run this PowerShell against
$exchangePowerShellServer = ""
# ------------------------- (END) Customize this section per your deployment -------------------------

if ($timeFrame -match "today") {
    # How we define today
    $startTime = (get-date -Hour 00 -Minute 00 -Second 00)
    $endTime = (get-date -Hour 23 -Minute 59 -Second 59)
    $subject = "Staff who sent over $minimumEmails e-mails for $startTime to now"

elseif ($timeFrame -match "yesterday") {

    # How we define yesterday
    $startTime = (get-date -Hour 00 -Minute 00 -Second 00).AddDays(-1)
    $endTime = (get-date -Hour 23 -Minute 59 -Second 59).AddDays(-1)
    $subject = "Staff who sent over $minimumEmails e-mails for $startTime to $endTime"

else {

    # Output parameter information if it isn't specified
    Write-Host "Please specify '-timeFrame today' or '-timeFrame yesterday' when invoking this script" -ForeGroundColor "Red"


# Debugging - This is useful if you're executiong the script under your account and need to test with different credentials
#$UserCredential = Get-Credential
#$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri -Authentication Kerberos -Credential $UserCredential

# Production - This assumes you're running this as a scheduled task under a user account with the proper credentials so we don't prompt for credentials
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$exchangePowerShellServer/PowerShell/ -Authentication Kerberos

# Assuming we've gotten here in the script start a Exchange session
Import-PSSession $Session -AllowClobber

# Check if the exception list exists, create it if it doesn't and then read the exception list into the script skipping blank lines and comments
if (!(Test-Path "$($scriptLocation)\exceptionList.txt")) {
    New-Item -path $($scriptLocation) -name exceptionList.txt -type "file" -value "# One entry per line, E-mail addresses are NOT case sensitive" | Out-Null
$exceptionList = Get-Content "$($scriptLocation)\exceptionList.txt" | Where-Object { $_ -notmatch "#" -or $_ -notmatch "" }

# Get a list of transport servers in case there is more than one
$transportServers = Get-TransportService

# Initialize the variable so we can append to it. Initial stats array is cast differently because we can't use "Group-Object" on an ArrayList and we can't use .Remove on a Array
$mailStats = @()
[System.Collections.ArrayList]$mailStatsToDelete = @()
[System.Collections.ArrayList]$mailStatsArrayList = @()

# Search the message tracking log on each transport server
foreach ($server in $transportServers) {

    # Search the message tracking log within a time frame on each transport server only looking for the 'SENDEXTERNAL' EventID
    $mailStats += Get-MessageTrackingLog -Server $ -Start $startTime -End $endTime -EventID "SENDEXTERNAL" -ResultSize Unlimited


# Filter out anyone that didn't send enough e-mails
$mailStats = $mailStats |Group-Object -Property Sender | Where-Object {$_.Group.Recipients.Count -gt $minimumEmails}

# Convert the Array to an ArrayList so we can use the .Remove method and so we can sort things later
foreach ($stat in $mailStats) {

    $line = "" | Select-Object Email,Total
    $line.Email = $stat.Name
    $line.Total = $stat.Group.Recipients.Count
    $mailStatsArrayList += $line


# If there are entries in the exception list grab all of the matches and store them in a seperate array
if ($exceptionList.Count -ne 0) {
    # Go through the list of exceptions and find any matches and store them
    foreach ($exception in $exceptionList) {

        foreach ($stat in $mailStatsArrayList) {

            if ($stat.Email -like $exception) {

                $mailStatsToDelete += $stat




    # Remove the matched exceptions from the final results
    foreach ($stat in $mailStatsToDelete) {



# Check and see if there are any results to report
if ($mailStatsArrayList.Count -ne 0) {

    # Sort and format the output into a table for the e-mail
    $results = $mailStatsArrayList | Sort-Object -Property Total -Descending | Format-Table -Property @{Expression={$_.Total};Label="Count";Width=15; Alignment="left"},@{Expression={$_.Email};Label="Sender"; Width=250; Alignment="left"} |Out-String -width 300

    # Send the e-mail
    $smtp = new-object Net.Mail.SmtpClient($smtpServer)
    $smtp.Send($from, $to, $subject, $results)


# Clean-up our session
Remove-PSSession $Session