Mapping Log4j Instances Across an Enterprise Network
An explanation of how to map Log4J instances across an enterprise Windows server network. The method can also be useful for locating other types of vulnerabilities or files across a corporate server infrastructure.
I originally published this article to perform a Log4j mapping across an enterprise network. It was written immediately after the vulnerability was discovered, at a time when not all security systems could detect it.
Today, most security systems can find and detect instances of this vulnerability with varying degrees of success. But the method I will present is still relevant for locating various items across a corporate server infrastructure.
The article content below is published as it originally appeared.
Introduction
The topic of the day in the IT industry is the security vulnerability discovered in the Java logging library.
Everyone managing a sprawling system of servers running various services and applications wants to know whether their systems are vulnerable. To find out, you first need to verify whether the software in question is installed on a server. You can then check - based on the vendor and version - whether the installed application is vulnerable or not.
Here I will share the approach I used to collect this information, along with scripts you can copy and use.
My team leader sent me a script intended to verify whether the Log4j library is installed on a machine. The script needed to run on all servers and the output needed to be collected to build a broad picture from which to proceed.
The script I received was written in Batch - the syntax language of the classic CMD shell. However, SCCM did not handle it well and could not run it on the servers. I packaged it in a file with a .bat extension - a Batch file, which is an executable script for CMD commands. To the servers I sent a PowerShell script that, among other things, calls that file and runs the script through it.
Our environment has multiple domains. In one domain it worked; in others, less so; in some, not at all.
Given this, I discarded that script and wrote a complete PowerShell script, which I distributed to the servers.
I then wrote a second script that collects the results from all servers in a given domain and aggregates them into a CSV file.
Let us walk through the steps with explanations.
The first script:
(Appears at the end as Appendix A.)
Distribution
This script is not run directly or as a file. I found the best way to distribute it across multiple different domains is to write the bulk of it as a variable containing a string. When the script runs on a server, it converts the string into an executable PowerShell file - a .ps1 file - and then runs it.
I discovered that a script that copies a pre-made file has difficulty reaching other domains, and even within the same domain does not reach all servers. A string that the script exports to a file works excellently.
Here is how it works:
- Open the SCCM console, go to Software Library, and at the bottom of the category list find Scripts.
- Create a new one and paste the contents of Appendix A into it. Complete the process - you now have a script in SCCM waiting for approval.
- Right-click and select Approve, then an approval wizard opens. Approve and complete the process.
- Now go to the Device and Collection category and find the Collection containing all servers (if one does not exist, you need to create it - we will not cover that here).
- Right-click on that Collection and from the menu that appears select Run Script.
In the panel that opens, select your script and run it. Wait until it finishes running on all servers.
How the script works:
What actions does the script perform?
-
Stores a script - a sequence of commands - as a string inside a variable.
-
Exports the variable to a PowerShell executable file.
-
Runs the file.
What does the launched file do? In other words, what commands did the string contain after it became a file?
-
Checks whether the
tempfolder exists on the C drive. -
If the folder does not exist, it creates it.
-
Creates inside it a folder named after the machine.
-
Checks what storage drives are present on the machine.
-
On each and every storage drive, searches for a file matching
log4j*.jar- meaning any file whose name starts withlog4j, regardless of what other characters follow, as long as the extension is.jar. -
The script stores in variables the following details: how many drives were searched, how many files were found, and information about each file found - such as its name and location.
-
The search results are exported to a CSV file in the
tempfolder on drive C of that server.
After the script was deployed via SCCM to all servers and ran its commands on each one, every server at the same path has a file containing the search results for log4j*.jar. It also checks whether the JndiLookup library is present inside the file.
Now we need to collect the data.
The second script:
(Appears at the end as Appendix B.)
This script should be run from PowerShell ISE as Administrator on a server hosting the domain’s Active Directory, preferably on a DC. Copy Appendix B and paste it into PowerShell ISE, adjust the lines containing the server search path and the path where you want to export the results, then run it.
How the script works:
What actions does the script perform?
- Queries Active Directory and loads into a variable (an array) all servers found in the OU defined as its SearchBase. You define this as follows:
Open Active Directory with Advanced Features view enabled, find the folder where all servers are located, right-click and select Properties. Go to the Attributes tab and find the attribute named Distinguished Name. Copy its value and use it as a quoted string for the SearchBase parameter. - Queries Active Directory again to collect all DCs from the Domain Controllers folder. As before, open the folder’s Properties and copy the Distinguished Name attribute to use as the search base for controllers.
- A loop then runs through the following sequence of operations for each server found.
- A check is performed for whether a result file exists on the server.
- The search results from the file are added to a master list.
- An additional list is built to verify whether results were found and collected from each server - for comparison and correction.
- Both lists are exported to CSV files - one with search results from all servers, one as a validation list.
Adjust file names for imported or exported files and compare the search results against the validation list.
The third script:
(Appears at the end as Appendix C.)
This script is not mandatory - it merely adds a parameter to the previous search results. The parameter indicates whether the JndiLookup library is contained within a given Log4j instance.
As noted, even if the file does not contain the library, it still needs to be updated. This script is designed to assign higher priority to instances of the file that do contain the library. For those who need to hand off remediation to other teams because the files belong to different applications, and find that the process is taking time, this lets them emphasize locations where the library is present.
In the worst case, the library can be removed from the file according to instructions I published in another article, which can be found in my article history or in a PDF file in the description of my current job.
Copy the script into PowerShell ISE on a DC and run it. Repeat this process for every domain in your environment.
How the script works:
What actions does the script perform?
- Import the file containing the search results from the entire domain - the file produced by the previous script (adjust the file name and location in the import command within the script).
- Add a column to the table. The column will contain True or False in answer to the question of whether the file contains the JndiLookup library.
- A loop iterates over every row in the file/table. The loop’s actions are described below.
- Create a temporary folder to which the file is copied.
- Extract the contents of the archive file into the temporary folder.
- Check among the extracted files whether the target library is present. The result is recorded in the appropriate cell.
- Delete the temporary folder along with all its contents to clean up for the next iteration of the loop.
- After the loop has finished processing all rows in the table, export the results to a CSV file.
The fourth script:
(Appears at the end as Appendix D.)
At this point we have result files for each domain. The following script consolidates all the files into a single clean, organized file.
Before running the script, collect the result files from all domains and place them in the same folder on the machine/server that will run the fourth script.
Then copy the script into PowerShell ISE and run it.
How the script works:
What actions does the script perform?
- Collect the names of all result files into an array variable - update the relevant lines in the script that reference the file location and names.
- Import all data from every file and add it to a single variable containing one large table.
- A loop iterates over every row in the table.
- A check is performed to determine whether the search results in that row contain a Log4j file location or an empty result because no files were found on that server.
- Empty rows are skipped; every row with a result is added to a new variable.
- The new variable is exported to a CSV file.
Linux:
Anyone with a tool that centrally manages Linux servers can run the following command on all of them and aggregate the output. Those without such a tool will need to go server by server manually and enter the following command:
find / -type f -name "*log4j*" 2>/dev/null
To export the results to a file, change it to the following command:
find / -type f -name "*log4j*" >>/tmp/res.txt
Reminder!
Before working with any of the scripts, copy it into PowerShell ISE and review what changes and adjustments are needed.
There will almost always be something to adjust - the path for searching servers in Active Directory, or references to files you want to process or pull data from.
Appendix A:
Deploy this script to workstations and run it on them. The script will collect a log of Log4j instances from that machine and save it to a file on drive C.
# Craete script in SCCM and put this script in there.
# Run this script on all servers in all domains you have in your organization.
$cmd = @'
if (!(Test-Path c:\temp)){
New-Item -Path c$ -Name temp -ItemType Directory
}
if (Test-Path c:\temp\$env:COMPUTERNAME) {
Remove-Item -Path c:\temp\$env:COMPUTERNAME -Recurse -Force
}
New-Item -Path c:\temp -Name $env:COMPUTERNAME -ItemType Directory -Force
$disk = Get-PSDrive -PSProvider FileSystem
$result = @()
$iter = 0
$files = 0
foreach ($x in $disk) {
$x
$res = Get-ChildItem -Path $x.Root -Recurse -Force -Filter log4j*.jar -ErrorAction SilentlyContinue
$res | Add-Member -type NoteProperty -Name 'ComputerName' -Value $env:COMPUTERNAME
$iter += 1
$result += $res
}
$result | select fullname, exists, ComputerName | Export-Csv -Path C:\temp\$env:COMPUTERNAME\res.csv -NoTypeInformation -Encoding UTF8
'@
$cmd | Out-File -FilePath C:\temp\test.ps1 -Encoding utf8
&'c:\temp\test.ps1'
Appendix B:
Run this script on a domain server, preferably on a DC. The script collects the result files from the various servers - the files produced by the Appendix A script.
<# Instructions
- Collect list of all servers in specific domain.
- Adjust the parameter "SearchBase" with Dinstinguished Name of every OU that contain servers in Active Directory.
- DistinguishedName is an attribute. can be found in properties of OU, when view of Advanced Features is checked in Active Directory.
- Adjust this script and run it separately on DC on every domain you have in your organization.
#>
# Collect list of all servers from Active Directory
$servers = Get-ADComputer -Filter * -SearchBase 'OU=Servers,DC=contoso,DC=co,DC=il' | select name
$servers += Get-ADComputer -Filter * -SearchBase 'OU=Domain Controllers,DC=contoso,DC=co,DC=il' | select name
#$servers += Get-ADComputer -Filter * -SearchBase 'CN=Computers,DC=contoso,DC=co,DC=il' | select name
$AllRes = @()
$list = @()
# Loop this procces to collect result from each server in the list.
foreach ($x in $servers) {
$y = $x.name
$path = $y + ".contoso.co.il"
$resContent = 0
# If there is result file on the server...
if (Test-Path \\$path\c$\temp\$y\res.csv){
# Add the server result to a general list.
$AllRes += Import-Csv -Path \\$path\c$\temp\$y\res.csv
}
# Make additional list to test if result found and collected from each server.
$node = New-Object PSObject
$node | Add-Member -type NoteProperty -Name 'Computer Name' -Value $y
$node | Add-Member -type NoteProperty -Name 'Valid' -Value (Test-Path \\$path\c$\temp\$y\res.csv)
#$node | Add-Member -type NoteProperty -Name 'Result Content' -Value ($resContent.Length)
#$node | Add-Member -type NoteProperty -Name 'Result' -Value $resContent
$list += $node
}
# Export data to files.
$list | export-CSV C:\Users\$env:USERNAME\Documents\list-domain.csv -NoTypeInformation -Encoding UTF8
$AllRes | export-CSV C:\Users\$env:USERNAME\Documents\AllRes-domain.csv -NoTypeInformation -Encoding UTF8
Appendix C:
Check in each search result whether the JndiLookup library is included in that instance.
<# Instructions
- JndiLookup.class is the biggest vulnerability in the archive.
- Althogh if JndiLookup.class is not there, there is other vulnerabilities in the archive.
- Finding if JndiLookup.class is there or not, just determing priority of handling the occurance of the archive.
- The script take the file of clean result and add the parameter wethere is JndiLookup.class is in that archive or not.
- Run that script on DC of every domain, proccessing the result-file collected from this domain.
#>
# Adjust according the location of your clean-result file.
$files = Import-Csv -Path C:\Users\$env:USERNAME\Desktop\AllRes-domain.csv
# Add the parameter determing if JndiLookup is in this archive.
$files | Add-Member -type NoteProperty -Name 'Vulnerable' -Value 0
# Loop on each line in the file, check if this archive contains JndiLookup.
foreach ($x in $files){
# Create a folder to extract the archive to.
New-Item -Path C:\Users\$env:USERNAME\Desktop\temp -Name sandbox -ItemType Directory
$localpath = $x.FullName.Replace(":\", "$\")
$path = '\\' + $x.computername + "\" + $localpath
# Copy the arcive to the new foder and extract it.
Copy-Item -Path $path -Destination C:\Users\$env:USERNAME\Desktop\temp\sandbox\check.zip
Expand-Archive -LiteralPath C:\Users\$env:USERNAME\Desktop\temp\sandbox\check.zip -DestinationPath C:\Users\$env:USERNAME\Desktop\temp\sandbox\
# Check if JndiLookup is in the archive.
if(Test-Path C:\Users\$env:USERNAME\Desktop\temp\sandbox\org){
$x.Vulnerable = Test-Path C:\Users\$env:USERNAME\Desktop\temp\sandbox\org\apache\logging\log4j\core\lookup\JndiLookup.class
}
# Delete the archive and extracted files, clean the folder for the next iteration of the loop.
$fullPath = (Resolve-Path "C:\Users\$env:USERNAME\Desktop\temp\sandbox").ProviderPath
[IO.Directory]::Delete($fullPath, $true)
#Remove-Item -Path C:\Users\$env:USERNAME\Desktop\temp\sandbox -Recurse -Force
}
# Export result to a file.
$files | Export-Csv -Path C:\Users\$env:USERNAME\Desktop\bedfiles-domain.csv -NoTypeInformation
Appendix D:
Collect and consolidate all search results into a single organized file.
<# Instructions
- Collect all result-files from all domains and put it in the same folder.
- Adjust the veriable $files with the real files names you have.
- Run the script and get all result from all domains, in one file.
- The secound loop should get rid of empty lines, add to the final list only where log4j were found.
--- !!! not sure if this script is needed after running the second version of the scripts.
If you got good and clean file of result from the second script of collecting the result,
If there is no empty lines without occurances\locations of log4j,
In this case ignore this script.
#>
# Put here the location of the result-files.
Set-Location C:\Users\$env:USERNAME\Documents\
# Adjust according to the files names.
$files = 'log4-domainA.csv', 'log4-domainB.csv', 'log4-domainC.csv', 'log4-domainD.csv', 'log4-domainE.csv'
$servers = 0
$servers = @()
# Loop collecting result from each file, adding it to one list.
foreach ($x in $files) {
$servers += Import-Csv -Path $x
}
# Should add to the final list only lines with results, filtering empty results.
$clean = @()
foreach ($x in $servers){
$y = [int]$x.'Result Content'
if (($x.'Computer Name'.Length -gt 2) -and ($y -gt 40)){
$clean += $x
}
}
# Export result to a file.
$clean | Export-Csv -Path C:\Users\$env:USERNAME\Desktop\FullClean.csv -NoTypeInformation