מיפוי מופעי Log4j

מיפוי מופעי Log4j

בשעתו פרסמתי את המאמר הזה כדי לבצע מיפוי Log4j ברשת ארגונית. זה היה מיד לאחר שהתגלתה הפגיעות הזו ולא כל המערכות ידעו לאתר אותה.

כיום רוב המערכות לאבטחת מידע יודעות למצוא ולאתר מופעים של הפגיעות הזו בדרגות שונות של הצלחה. אבל הדרך שאציג עדיין רלוונטית לאיתור גורמים שונים ברשת שרתים ארגונית.

להלן תוכן המאמר כפי שהתפרסם.

הקדמה

שיחת היום בענף ה-IT זו פרצת האבטחה שהתגלתה, בספריית רישום הלוגים של ג'אווה.

כל מי שמנהל מערכת מסועפת של שרתים ובהם שירותים ואפליקציות שונות, רוצה לדעת האם המערכות שלו פגיעות. ולשם כך, צריך לוודא קודם כל האם המערכת המדוברת יושבת בשרת. לאחר מכן ניתן לבדוק לפי היצרנים והגרסה האם האפליקציה המותקנת פגיעה, או שאין בה סכנה.
כאן אחלוק עמכם את הדרך שעשיתי על מנת לאסוף את המידע הזה, יחד עם סקריפטים שאפשר להעתיק ולהשתמש בהם.

אז ראש הצוות שלי שלח לי סקריפט שאמור לוודא אם במכונה יושבת הספריה Log4j. את הסקריפט צריך להריץ בכל השרתים, ולאסוף את הפלט כדי לקבל תמונת מצב רחבה ומשם להתקדם.

הסקריפט שקיבלתי היה כתוב ב-Batch, שזו שפת התחביר של CMD הישן והטוב. אבל SCCM לא הסתדר איתו כל כך טוב, ולא הצליח להריץ אותו בשרתים. אז סגרתי אותו בקובץ עם סיומת bat – מה קוראים אותו Batch file – שהוא קובץ הרצה לסקריפטים של CMD. לשרתים שלחתי סקריפט של PowerShell שבין השאר קורא לאותו קובץ ומריץ באמצעותו את הסקריפט.

במערכת שלנו יש כמה דומיינים. בדומיין אחד זה עבד, באחרים פחות. בחלק בכלל לא.
היות וכך, זרקתי את הסקריפט ההוא וכתבתי לי סקריפט PowerShell על מלא, אותו הפצתי לשרתים.
לאחר מכן כתבתי סקריפט אחר, שאוסף את הנתונים מכל השרתים בדומיין מסויים, ואוסף אותם לקובץ CSV.
נעבור על השלבים בליווי הסברים.

הסקריפט הראשון:

(יבוא בסוף כנספח א').

הפצה

את הסקריפט הזה לא מריצים ישירות או כקובץ. גיליתי כי הדרך הטובה ביותר להפיץ אותו לכמה דומיינים שונים, היא לכתוב את הרוב כמשתנה המכיל מחרוזת. ברגע שהסקריפט רץ בשרת, משם הוא הופך את המחרוזת לקובץ הרצה של PowerShell – קובץ ps1, ואז מריץ אותו.
גיליתי שסקריפט שמעתיק קובץ מוכן, מתקשה להגיע לדומיינים אחרים, וגם באותו דומיין לא מגיע לכל השרתים. מחרוזת שהסקריפט מייצא לקובץ, זה כבר עובד מצוין.

וכך זה עובד:

  1. פותחים את הקונסולה של SCCM, הולכים ל-Software Library, ושם בתחתית רשימת הקטגוריות יש Scripts.
  2. יוצרים אחד חדש ומדביקים בתוכו את התוכן של נספח א'. מסיימים את התהליך, ואז יש לנו ב-SCCM סקריפט שממתין לאישור.
  3. קליק ימני ולחיצה על Approve, ואז נפתח אשף לאישור הסקריפט. מאשרים ומסיימים את התהליך.
  4. עכשיו עוברים לקטגוריה של Device and Collection, ושם מוצאים את ה-Collection שמכיל את כל השרתים (אם אין, צריך ליצור אחד כזה. לא נעבור על זה פה).
  5. קליק ימני על אותו Collection, ובתפריט שעולה בוחרים Run Script.

בחלונית שנפתחת בוחרים את הסקריפט שלנו, ומריצים אותו. ממתינים עד שיסיים לרוץ על כל השרתים.

פעולת הסקריפט:

איזה פעולות מבצע הסקריפט?

  1. מכניס סקריפט – רצף של פקודות – כמחרוזת אל תוך משתנה.
  2. מייצא את המשתנה לקובץ הרצה של PowerShell.
  3. מפעיל את הקובץ.

    מה עושה הקובץ המופעל? כלומר, המחרוזת שיצאה מהמשתנה והפכה לקובץ, איזה פקודות היא הכילה?

  4. בודק אם בכונן C קיימת התיקיה temp.
  5. אם התיקיה איננה קיימת, הוא יוצר אותה.
  6. יוצר בתוכה תיקיה הנושאת את שם המכונה.
  7. בודק איזה כונני אחסון יש במכונה.
  8. בכל אחד ואחד מכונני האחסון רץ חיפוש עבור קובץ log4j*.jar. שזה אומר כל קובץ שמתחיל ב-log4j. לא משנה איזה עוד תווים יש בשם הקובץ, והעיקר שהסיומת שלו היא .jar.
  9. הסקריפט שומר למשתנים את המאפיינים הבאים: בכמה כוננים בוצע חיפוש, כמה קבצים מצא, מידע על כל קובץ שמצא – כמו שם הקובץ והמיקום שלו וכו'.
  10. תוצאות החיפוש יוצאות לתוך קובץ CSV בתיקיית temp על כונן C באותו השרת.

לאחר שהסקריפט ירד בכל השרתים דרך SCCM והריץ בכל אחד מהם את הפקודות, יש לנו בכל שרת באותו נתיב, קובץ המכיל תוצאות חיפוש של log4j*.jar. בנוסף, בודק האם ספריית JndiLookup קיימת בתוכו.
עכשיו צריך לאסוף את הנתונים.

הסקריפט השני:

(יבוא בסוף כנספח ב').

את הסקריפט הכדאי להריץ מתוך PowerShell ISE Administrator, בשרת שעליו יושב Active Directory של הדומיין. מעתיקים את נספח ב' ומדביקים בתוך PowerShell ISE, מתאימים את השורות המכילות את הנתיב שבו יושבים השרתים ואת הנתיב שאליו רוצים לייצא את התוצאה, ואז מריצים.

פעולת הסקריפט:

איזה פעולות מבצע הסקריפט?

  1. הולך ל-Active Directory ולוקח לתוך משתנה (מסוג מערך) את כל השרתים הנמצאים ב-OU שהוגדר לו כ-SearchBase. את זה מגדירים בצורה הבאה:
    פותחים Active Directory בתצוגה מורחבת, מוצאים את התיקיה שבה יושבים כל השרתים, מבצעים קליק ימני ולוחצים על Properties. שם הולכים ללשונית Attributes ומוצאים את ה-Attribute בשם distinguished name. מעתיקים את התכולה שלו בתוך גרשיים כפרמטר של SearchBase.
  2. הולך שוב ל-Active Directory ואוסף את כל ה-DC מהתיקיה Domain Controllers. כמו בפעם שעברה, צריך לפתוח את ה-Properties של התיקיה ולהעתיק את ה-Attribute שנקרא distinguished name ולשים אותו בתור בסיס החיפוש לשרתים.
  3. כעת רצה לולאה שעוברת על סדר הפעולות הבאות בכל אחד מהשרתים שנמצאו.
  4. מתבצעת בדיקה האם קובץ תוצאות חיפוש קיים על השרת.
  5. תוצאות החיפוש מן הקובץ מתווספות לרשימה.
  6. רשימה נוספת בודקת אם יש שרתים שלא הכילו קובץ תוצאות חיפוש, להשוואה ותיקון.
  7. יצוא שתי הרשימות לקבצי CSV – רשימת תוצאות חיפוש מכל השרתים, ורשימת אימות.

יש להתאים שמות לקבצים מיובאים או מיוצאים, ולבצע השוואה של תוצאות החיפוש מול רשימת האימות.

הסקריפט השלישי:

(יבוא בסוף כנספח ג').

הסקריפט הזה איננו הכרחי, הוא רק מוסיף פרמטר לתוצאות החיפוש הקודמות. הפרמטר קובע האם בקובץ Log4j שנמצא, מכיל את ספריית JndiLookup.

כאמור, גם אם הקובץ אינו מכיל את הספריה, עדיין חשוב לעדכן אותו. הסקריפט נועד לקבוע עדיפות גבוהה יותר לטיפול במופעים של הקובץ המכילים את הספריה. מי שצריך להעביר לגורמים אחרים את הטיפול בספריות כי הן נוגעות לאפליקציות שונות, ורואה שהטיפול מתעכב, יכול לתת דגש על המקומות בהם מופיעה הספריה.

במקרה הכי גרוע ניתן להסיר את הספריה מן הקובץ בהתאם להוראות שהעליתי במאמר אחר, וניתן למצוא בהיסטוריית המאמרים שלי, או בקובץ PDF שנמצא בתיאור העבודה הנוכחית שלי.

מעתיקים את הסקריפט לתוך PowerShell ISE על DC ומריצים אותו. חוזרים על התהליך הזה בכל דומיין שיש במערכת.

פעולת הסקריפט:

איזה פעולות מבצע הסקריפט?

  1. יבוא של הקובץ המכיל את תוצאות החיפוש מכל הדומיין. הקובץ שיצא מהסקריפט הקודם (יש להתאים בסקריפט את שם ומיקום הקובץ, בפקודה שמייבאת אותו).
  2. הוספת עמודה לטבלה. העמודה תכיל True או False בתשובה לשאלה האם הקובץ מכיל את הספריה JndiLookup.
  3. לולאה שעוברת על כל השורות בקובץ/טבלה. פעולות הלולאה בשורות הבאות.
  4.  יצירת תיקיה זמנית, אליה מועתק הקובץ.
  5. חילוץ של עותק קובץ הארכיון אל תוך התיקיה הזמנית.
  6. בדיקה בקבצים שחולצו האם מופיעה הספריה המבוקשת. התשובה מתווספת למשבצת הרצויה.
  7. מחיקה של התיקיה הזמנית על כל תכולתה, על מנת לנקות את השטח עבור הסיבוב הבא של הלולאה.
  8. לאחר שהלולאה סיימה לרוץ על כל השורות בטבלה, יצוא של התוצאות לקובץ CSV.

הסקריפט הרביעי:

(יבוא בסוף כנספח ד').

כעת יש לנו קבצי תוצאות עבור כל דומיין. הסקריפט הבא יאחד את כל הקבצים לקובץ אחד נקי ומסודר.

לפני שמריצים את הסקריפט צריך לאסוף את קבצי התוצאות מכל הדומיינים ולהניח אותם בתיקיה אחת, במחשב/שרת שמריץ את הסקריפט הרביעי.

אז מעתיקים את הסקריפט לתוך PowerShell ISE ומריצים אותו.

פעולת הסקריפט:

איזה פעולות מבצע הסקריפט?

  1. איסוף שמות כל קבצי התוצאות לתוך משתנה מסוג מערך – יש לעדכן את השורות המתאימות בקובץ, המתייחסות למיקום הקבצים ולשמותיהם.
  2. יבוא כל הנתונים מתוך כל קובץ, והוספתם למשתנה אחד שמכיל טבלה אחת גדולה.
  3. לולאה שעוברת על כל שורה בטבלה.
  4. בדיקה האם תוצאות החיפוש באותה השורה מכילות תוצאה של מיקום קובץ Log4j או תוצאה ריקה כי לא נמצאו קבצים בשרת.
  5. התעלמות משורות ריקות, הוספת כל שורה בעלת תוצאה למשתנה חדש.
  6. יצוא של המשתנה החדש לקובץ CSV.

Linux:

מי שיש לו כלי השולט במרוכז בשרתי הלינוקס, יוכל להריץ את הפקודה הבאה בכל השרתים ולרכז את הפלט. מי שלא, יצטרך לעבור ידנית שרת-שרת ולהזין את הפקודה הבאה:

find / -type f -name "*log4j*" 2>/dev/null

אם רוצים לייצא את התוצאות לקובץ, יכול לשנות את זה לפקודה הבאה:

find / -type f -name "*log4j*" >>/tmp/res.txt

תזכורת!

לפני עבודה עם כל אחד מן הסקריפטים, כדאי להעתיק אותו אל תוך PowerShell ISE ולבחון איזה שינויים והתאמות צריך לבצע בו.
כמעט תמיד יעלה הצורך לבצע איזה שהוא שינוי, בנתיב לחיפוש השרתים ב-Active-Directory או בהפניות לקבצים שאותם רוצים לעבד או לשאוב מהם נתונים.

נספח א':

את הסקריפט הזה מפיצים לתחנות ומריצים אותו עליהן. הסקריפט יאסוף רישום של מופעי Log4j מאותה התחנה וישמור בקובץ על כונן 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' 

נספח ב':

את הסקריפט מריצים על שרת בדומיין, רצוי על DC. הסקריפט אוסף מהתחנות השונות את קבצי התוצאה מהסקריפט של נספח א'.

<# 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 

נספח ג':

בדיקה בכל אחת מתוצאות החיפוש האם הספריה JndiLookup כלולה באותו מופע.

<# 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 

נספח ד':

איסוף וריכוז של כל תוצאות החיפוש לקובץ אחד מסודר.

<# 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

כתיבת תגובה