שלום עליכם.
היום התירוץ שלי לדבר על PowerShell, זה תעודות הצפנה – Certificate.
בעדכון הכללי של מיקרוסופט לחודש מאי 2022, נכלל תיקון שמוסיף שדה מסוים לתעודות. השדה הזה מאפשר לקשר בין תעודה למשתמש בצורה חזקה ואמינה יותר מהצורה שבה השתמשו עד היום. השדה הזה מכיל את ה-SID של המשתמש ומבטיח אימות חזק ואמין יותר.
עד היום כדי לקשר בין תעודה למשתמש, התעודה היתה מונפקת על פי שם משתמש, וכך המערכת ידעה שהמשתמש הזה קשור לתעודה וניתן לסמוך עליו. במשך השנים התגלו בעיות בצורת הקישור הזו בין תעודה למשתמש. תוקפים הצליחו להתחזות לשם המשתמש בצורותיו השונות, וכך יכלו להשתמש בתעודה שלו שאימתה את זהותם הכוזבת מול Active-Directory.
בארגונים שמקבלים את העדכונים מיד לאחר שהם יוצאים, התעוררו הרבה רעש ומהומה. משתמשים ומחשבים נדרשו לאמת את עצמם באמצעים שעדיין לא עמדו לרשותם, או בגלל כשלים שיש לפעמים בעדכונים של מיקרוסופט – שאז מוציאה עדכון נוסף כדי לתקן את העדכון הקודם.
בינתיים, אנשי IT נדרשו להחזיר את האימות החדש הזה ממצב אכיפה למצב הכנה, עד שיותקן העדכון-לעדכון, ועד שכל המשתמשים והמחשבים עברו להשתמש בתעודות החדשות.
ניתן לראות בלוגים של ה-DC את אלה שעדיין מתחברים עם התעודות הישנות. אם במצב אכיפה, הלוג מצביע על שגיאה. אם במצב הכנה, הלוג מצביע על אזהרה. אבל כדי לעבור למצב אכיפה, נרצה לקבל תמונה מקיפה יותר, ואולי גם לעקוב אחרי ההליך שעל פיו הוחלט לנקוט במטרה לעבור לתעודות החדשות – אלה שמכילות את השדה החדש.
אז בתור התחלה, איך בודקים אם תעודה קיבלה את השדה החדש?
1. דרך ממשק המשתמש הרגיל, פותחים כמנהל את שורת הפקודה.
2. מקישים את הפקודה MMC.
3. בקונסולה שנפתחת, לוחצים בתפריט File על הוספה או הסרה של Snap-In.
4. מתוך התפריט של Snap-In שניתן לטעון, בוחרים Certificate.
5. אז עולה הבחירה בין תעודות משתמש או תעודות של המחשב. ניתן לבחור אחת מהן.
6. כשנטען ה-Snap-In נכנסים לתיקיה Personal ושם לתוך התיקיה Certificate.
7. שם תהיה תעודה הנושאת את שם המשתמש או המחשב. לחיצה כפולה עליה תפתח את נתוני התעודה.
8. עוברים ללשונית Details. זו מן טבלה בסגנון Key-Value.
9. יורדים למטה ומחפשים שדה שבמקום שם רגיל, מכיל את סדרת המספרים הבאה – '1.3.6.1.4.1.311.25.2'
10. אם השדה הזה נמצא שם, יש לכם את התעודה החדשה. אם לא, ניתן לבקש תעודה חדשה לפי השלבים הבאים:
11. קליק ימני להקפיץ תפריט.
12. לבחור All Tasks ואז Request Certificate with new key.
13. לעבור את התהליך הקצר שבסופו לוחצים Enroll, ויש לכם תעודה חדשה.
14. אם גם בתעודה החדשה אין את השדה, העדכון טרם הותקן אצלכם בשרתים.
מעקב אחר כלל תעודות המשתמשים והמחשבים:
כדי לעקוב אחר התמונה הכללית בארגון, כתבתי סקריפט שמושך משרת התעודות את כל התעודות שהונפקו, ובורר מתוכן את אלה המיועדות לאימות מחשבים ומשתמשים (בניגוד לכאלה המשמשות לאימות שרת Web, או מטרות אחרות), ואז מבצע סינונים נוספים.
לבסוף אמורות להתקבל טבלאות המכילות פרטים על תעודות המשתמשים והמחשבים, ואיזה מהן מכילות את השדה החדש.
להלן אסביר את פעולת הסקריפט, שיבוא לבסוף כנספח.
הסקריפט מכיל תיעוד מפורט ברמה של נודניק, ומסביר את הפעילות של כל שלב.
כאן אתן הסבר בעברית על הזרימה של התהליך, ואיך אפשר לשנות או להתאים לפי רצון המשתמש בסקריפט.
פונקציה ראשונה – Get-Certs:
שורות 4-48.
עקרונית, הפונקציה הזו נועדה למשוך נתונים של תעודות משרת התעודות.
במקרה שלנו, ישנה התאמה מלאה למטרה לשמה נכתב הסקריפט כולו – למשוך נתונים ספציפיים על תעודות שהופקו במטרה לגלות אלה מהן מכילות את השדה החדש.
אבל מי שרוצה יוכל להתאים את הנתונים שהוא רוצה למשוך לפי רצונו.
כאן אתן הסבר מפורט על הפעולות שבמחצית הראשונה של הפונקציה, כדי שמי שירצה יוכל לבצע את השינויים הדרושים לו.
כפי שניתן לראות בתיעוד, שתי השורות הראשונות (שורות 7-8) נועדו לפתוח חיבור לשרת התעודות. המשתמש צריך לדעת מה שם שרת התעודות שלו, ושם לפתוח את מנהל התעודות דרך תפריט הכלים שב-Server Manager.
שירות התעודות:
אז נפתחת קונסולה של Certificate Authority, ותחתיה בהיררכיה יש את מנהל התעודות עצמו – על פי שמו. את השם הזה אנחנו צריכים להזין בסקריפט בשורה שמונה, במתכונת הכתובה שם. משמאל לימין – שם שרת, לוכסן, שם מנהל התעודות.
הפיסקה הבאה קובעת את הנתונים שימשוך הסקריפט ממסד הנתונים של מנהל התעודות.
מי שפותח את הקונסולה של מנהל התעודות ובוחר בתיקיה Issued Certificates, יכול לראות לפניו טבלה מוצגת ובה עמודות רבות שכל אחת מציגה סוג אחר של נתונים, וכל שורה מייצגת תעודה אחרת.
בתפריט התצוגה של הקונסולה ישנה פקודה Add/Remove column. לחיצה עליה פותחת חלונית שבה ניתן להתאים את התצוגה כך שתכיל רק את עמודות הנתונים שהמשתמש רוצה לראות.
מתוך כל אלה המשתמש צריך לבחור איזה נתונים הוא צריך. לאחר מכן להזין בתוך הסוגריים בשורה 13 את מספר העמודות שבחר, ואת השורות הבאות להתאים לפי זה. אם צריך לצמצם או להגדיל את מספר השורות הבאות בהתאם למספר העמודות, זה מה שעושים.
לדוגמה:
מי שבחר 8 עמודות, יזין את הספרה 8 בתוך הסוגריים בשורה 13. לאחר מכן בשורות הבאות יתן שורות עבור אינדקס מ-0 ועד 7. כאשר בסוגריים בסוף כל שורה, לאחר False והפסיק, ישים בתוך מרכאות את שם העמודה שבחר.
לאחר מכן צריך להציב את כל האינדקסים האלה בשורה, כשלבסוף Pipeline שמכניס את כולם לתצוגת העמודות המבוקשת – בדומה לדוגמה הנוכחית שיש לנו בשורה 19.
שורה עשרים נותנת לנו דוגמה לסינון של התצוגה, על פי מידע מאחת העמודות. אני בחרתי לסנן את התוכן בטבלה כך שיגיעו אלי רק שורות שבהן יש את הערך 20 באינדקס 4.
מה זה אומר?
מי שיחזור לקונסולה של מנהל התעודות ויבחר להציג את העמודה של Request Disposition, יראה שהמספר 20 מייצג Isuued Certificates. כך שאם אני רוצה להציג רק Issued Certfucate, אני מציג את הפקודה SetRestriction שהיא פקודת הסינון, ואומר לה לסנן לפי העמודה של אינדקס 4, להציג רק את מה שיש שם את הערך 20.
דוגמה אחרת – אם הייתי רוצה לסנן ולהציג רק תעודות שהופקו על שם Yosi Cohen, הייתי יכול לשים בתחילת הסוגריים את אינדקס 0 שמכיל את שם המשתמש, ובסוף הסוגריים במקום 20 לשים בתוך מרכאות את השם שלי כפי שמופיע בתעודות שלי. כי העמודה של Requester Name מכילה שמות משתמשים בצורה של NetBios. כלומר – Domain\User. היות ומדובר במחרוזת, צריך לשים בתוך מרכאות – בניגוד למספר 20 שהיה שם קודם.
כעת, לאחר שהוסבר בפירוט כיצד פועלת בניית והגדרת הטבלה שמושכים מתוך מסד הנתונים של מנהל התעודות, אפשר לדעת איך להתאים את פעולת הפונקציה כך שתציג למשתמש כל מידע שהוא צריך משם בהתאם לצרכיו.
המשך הפונקציה פשוט יותר.
לאחר מכן מתבצעת לולאה מקוננת שעוברת על כל השורות בטבלה שהוגדרה (לפי הסינון), ובכל שורה עוברת על כל העמודות שהוגדרו (לפי בחירת העמודות שהתבצעה). וכך המשתנה $AllCerts משורה 27 אוסף אליו מערך, כשכל פריט במערך מכיל נתונים על תעודה אחרת.
מי ששינה את הגדרת העמודות כפי שהסברתי לעיל, יצטרך לשנות גם את השורות האחרונות בפונקציה, שלוקחות כל שם של עמודה ומשנות אותו לשם בן מילה אחת. כעת שמות העמודה מוגדרים לפי העמודות שבחרתי. מי שיבחר עמודות אחרות, יצטרך לשנות את הפקודה הזו בהתאם.
לבסוף בשורה 47 הפונקציה מחזירה את רשימת התעודות – לפי סינון. רק תעודות שעוד לא פג תוקפן.
פונקציה שניה – Add-CertExtentions:
שורות 53-70.
פונקציה זו יכולה אמנם לשרת גם מטרות אחרות, למי שרוצה. אבל כאן היא נועדה לשרת מטרה אחת – לאסוף מכל תעודה את המידע שמכיל את השדות שביניהם אמור להיות השדה החדש.
מתוך כל הנתונים שמציגה הטבלה בשרת התעודות, לא הצלחתי למצוא איפה רואים את השדות האלה.
פונקציה זו לוקחת את הקוד הבינארי של התעודה – כפי שמופיעות תעודות בטקסט מגובב, ומתרגמת אותו לאובייקט של תעודה. תחת האובייקט הזה ניתן למצוא את השדות שביניהם אמור להיות השדה החדש.
הפונקציה מקבלת את המערך של נתוני התעודות, מוסיפה לו עמודה חדשה, ובעמודה זו מזינה עבור כל תעודה את כל השדות שמצאה בקטגוריית Extentions של התעודה הזו.
הפונקציה מחזירה את המערך החדש – טבלה כמעט זהה לקודמת, בתוספת עמודה חדשה של הרחבות.
פונקציה שלישית – Insert-Info:
שורות 76-94.
הפונקציה הזו מקבלת טבלה של נתוני תעודות, ואז קוראת לפונקציה הקודמת (השניה) שתוסיף לטבלה את העמודה של ההרחבות.
לאחר מכן הפונקציה הנוכחית (השלישית) מעבדת את העמודה המכילה את שם המשתמש. אם קודם העמודה הכילה שם משתמש בתבנית NetBios הכוללת את הדומיין, כעת העמודה מכילה את שם המשתמש לבדו.
אז הפונקציה מוסיפה לטבלה שתי עמודות נוספות. בעמודה הראשונה הפונקציה מכניסה עבור כל תעודה את הערך הבוליאני שאומר האם התעודה מכילה את השדה החדש, ובעמודה השניה את מספר הימים שנותרו עד תפוגת התעודה.
הפונקציה מחזירה את הטבלה עם כל השדות החדשים.
פונקציה רביעית – Group-Data:
שורות 101-138.
כל הפונקציות שלעיל נתנו לנו המון מידע על תעודות שונות. העניין הוא שמשתמש יכול להתחבר ממחשבים שונים, ועבור כל אחד מהם השרת ינפיק לו תעודת אימות משתמש. מדובר בתפזורת גדולה של נתונים שמתרכזים יותר בפרטים הקטנים ופחות בתמונה הכללית.
כדי לקבל תמונה כללית יותר, הפונקציה הרביעית מקבלת טבלה של נתוני תעודות, ומרכזת אותן תחת קבוצות. כל התעודות שהונפקו עבור אובייקט מסויים (אם זה מחשב/שרת או משתמש), מרוכזות תחת אותה הקבוצה.
בנוסף הפונקציה מתשאלת את ה-Active-Directory כדי לקבל ממנו עוד פרטים על האובייקט.
כך הפונקציה יוצרת טבלה חדשה שנותנת לנו את הנתונים הבאים:
1. שם אובייקט.
2. כמה תעודות נמצאו בסך הכל עבור האובייקט.
3. כמה מתוכן מכילות את השדה החדש.
4. כמה מתוכן אינן מכילות את השדה החדש.
5. תאריך ההתחברות האחרון של האובייקט לרשת הארגונית.
6. כמה ימים עברו מאז ההתחברות האחרונה של האובייקט.
7. האם האובייקט מופיע כפעיל או כמושבת ב-Active-Directory.
מהלך הסקריפט עצמו:
מתחילה מוגדרים משתנים המכילים את הקוד של תבנית התעודה שמעניינת אותנו. בהתאם לאלה נסנן אחר כך את הנתונים ונעבור רק על תעודות השייכות לתבניות אלה. את קוד התבנית ניתן למצוא במנהל התעודות תחת העמודה Certificate Template, או בתוך כל תעודה בלשונית Datails.
גם במנהל התעודות וגם בתוך התעודה עצמה, בתחילה יוצג שם התבנית. אבל עבור הסקריפט צריך רק את הקוד המספרי שבתוך הסוגריים. להעתיק ולהציב במשתנה.
בסקריפט הנוכחי הוגדר משתנה עבור תעודת-משתמש ועוד משתנה עבור תעודת-מחשב.
ישנן תבניות רבות נוספות כמו תבניות שרת של אתרי אינטרנט, תבניות עבור LDAP, קרברוס, תבניות בהתאמה אישית ועוד ועוד. מי שרוצה לחפש ולעבד תעודות מסוג אחר, צריך לקבוע משתנה עם קוד התבנית של אותו סוג תעודה.
לאחר שהוגדרו סוגי התעודות, מתבצעת קריאה לפונקציה הראשונה. זו מתחברת לשרת התעודות ומוציאה טבלה כללית של תעודות שהונפקו, הטבלה יושבת בתוך משתנה.
הסקריפט מבצע סינון מתוך הטבלה ומציב לתוך משתנה אחד טבלת-תעודות-משתמש, ולתוך משתנה אחר – טבלת-תעודות-מחשב.
לאחר מכן הסקריפט קורא לפונקציה שתכניס את ההרחבות ושאר הנתונים לתוך שתי הטבלאות.
לבסוף מתבצע יצוא של ארבע טבלאות לארבע קבצים.
כל סוג תעודה מייצר שני קבצים – קובץ אחד ובו המידע המורחב והפרטני עבור כל תעודה, קובץ שני ובו התמונה הכללית של כמות התעודות עבור כל אובייקט.
הסקריפט נחתם בשתי פקודות שמנתקות את הקשר עם שרת התעודות.
הקוד:
# This function connecting to CA server and retrieving certificates details
# Specificly here, issued certificates.
# The function returns an array object of all issued certificates.
function Get-Certs {
# Open Object to View the CA DataBase, and connect to it.
$CertDB = New-Object -ComObject CertificateAuthority.View
$CertDB.OpenConnection("CA-Server-Name\CA-Name")
# Choose which columns to get, and filter results by index4 - which represents the different folders in the CA.
# 20 is the code for Request Disposition of Issued Certificates.
# Means that we want to get only issued certificates, not requests or others.
$CertDB.SetResultColumnCount(5)
$index0 = $CertDB.GetColumnIndex($false, "Requester Name")
$index1 = $CertDB.GetColumnIndex($false, "Certificate Template")
$index2 = $CertDB.GetColumnIndex($false, "Certificate Expiration Date")
$index3 = $CertDB.GetColumnIndex($false, "Binary Certificate")
$index4 = $CertDB.GetColumnIndex($false, "Request Disposition")
$index0, $index1, $index2, $index3, $index4 | %{$CertDB.SetResultColumn($_)}
$CertDB.SetRestriction($index4,1,0,20)
# Open access to the table defined above. Begin before the top row and before the first column.
$CertDBRow = $CertDB.OpenView()
$CertDBRow.Reset()
# Array to receive all certs details.
$AllCerts = @()
# Go through all rows
for ($x = 0; $CertDBRow.Next() -ne -1; $x++) {
# Get access to all column in the row
$cell = $CertDBRow.EnumCertViewColumn()
# Object to receive all values of the cells in this row
$cert = New-Object -TypeName PsObject
# Go through all columns
for ($x = 0; $cell.Next() -ne -1; $x++) {
# Add to the array object all values in the row. header for each value got the display name of the column in the DB.
$cert | Add-Member -MemberType NoteProperty -Name $cell.GetDisplayName() -Value $cell.GetValue(1) -Force
}
# Add the row values - which represent 1 certificate, to an array of certificates.
$AllCerts += $cert
}
# Change headers names. make it easier to access and write it.
$new = $AllCerts | Select @{N='Name'; E={$_.'Requester Name'}}, @{N='Template' ;E={$_.'Certificate Template'}}, `
@{N='Expiry' ;E={$_.'Certificate Expiration Date'}}, @{N='Binary' ;E={$_.'Binary Certificate'}}
# Return an array of all certificates information.
return $new | ?{($_.Expiry - (Get-Date)).days -gt 0}
}
# This function decoding the binary certificate within the certificates, in manner to retrieve certificate extentions data.
# The function receive an array of certificates, and add the extentions data to all of them.
# The function returns an array of certs - include extentions data.
function Add-CertExtentions {
param ($certs)
# Add column for certificate extention data.
$certs | Add-Member -MemberType NoteProperty -Name 'Extentions' -Value $null -Force
foreach ($x in $certs) {
# Create object handeling certificate data.
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
# Takes the hash of the binary certificate and torn it into a single string.
$binary = ($x.Binary.Split("`n")) -join ''
# Decode the base64 certificate from the string to retrive certificate data. makes it certificate object.
$cert.Import([Convert]::FromBase64String($binary))
# Insert certificate extention data into the new cell created above.
$x.Extentions = $cert.Extensions.oid
}
# Returns all certificates with the extention data included
return $certs
}
<# This function proccess the certificate data and return specific fata required.
The function receive list of certificate, use another function to add extention data and proccess it.
The function returns a table, telling each cert weather it's contains the new OID, when it's expires - and then
will renew itself, etc. #>
function Insert-Info {
param ($list)
# Call function to add extentions details of the certs
$ExtendedCerts = Add-CertExtentions -certs $list
# Filter the name of user or computer own that certificate, to get the name identity like in Active-Diretory.
$ExtendedCerts | %{$_.Name = $_.Name.split("\")[1]}
# Add cells for the information if the certificate got the new OID and days to expiration.
$ExtendedCerts | Add-Member -MemberType NoteProperty -Name 'NewCert' -Value $null -Force
$ExtendedCerts | Add-Member -MemberType NoteProperty -Name 'DaysToExpiry' -Value $null -Force
$date = Get-Date
foreach ($x in $ExtendedCerts) {
# For each certs, insert info to the new cells created above
$x.NewCert = '1.3.6.1.4.1.311.25.2' -in $x.Extentions.value
$x.DaysToExpiry = ($x.Expiry - ($date)).days
}
# Returns the list of all certs, includes if the new OID is there.
return $ExtendedCerts
}
<# This function gives the final graining to the data.
User or Computer can have more than a single certificate. to concetrate all the information regarding a user or computer,
this function groups all certificates related with the user (or computer), and count how many of it contains the new OID,
and more data.
The function receive an array of certificates info, and return an array of users/computers info, related with certs. #>
function Group-Data {
param($list, $type)
# Group all certificates issued for this name.
$groups = $list | Group-Object -Property name
# Add some properties to the array, to give good picture about the users/computers and their crtificates.
$groups | Add-Member -MemberType NoteProperty -Name 'LastLogOn' -Value $null -Force
$groups | Add-Member -MemberType NoteProperty -Name 'DaysFromLastLogOn' -Value $null -Force
$groups | Add-Member -MemberType NoteProperty -Name 'Enabled' -Value $null -Force
$groups | Add-Member -MemberType NoteProperty -Name 'NewCerts' -Value $null -Force
$groups | Add-Member -MemberType NoteProperty -Name 'OldCerts' -Value $null -Force
$groups | Add-Member -MemberType NoteProperty -Name 'Type' -Value $null -Force
$date = Get-Date
# Go through all groups to check and add information to each of them.
foreach ($x in $groups) {
# Try get user/computer info from Active-Directory
try {
if ($type -eq 'user') {
$adinfo = Get-ADUser -Identity $x.Name -Properties Enabled, lastLogonTimestamp
} else {$adinfo = Get-ADComputer -Identity ($x.Name.Split("$")[0])}
# If the AD query failes, means that the subject is not there anymore. mark as irelevant.
} catch {
$adinfo = '' | select enabled, lastLogonTimestamp
$adinfo.enabled = 'Not Found'
$adinfo.lastLogonTimestamp = 0
}
# Insert data into the properties - if the user enable/disable, last time he logged on,
# hoe many of it's certificates have the new OID, etc.
$x.Enabled = $adinfo.enabled
$x.LastLogOn = [DateTime]::FromFileTime($adinfo.lastLogonTimestamp)
$x.DaysFromLastLogOn = -(($x.LastLogOn - ($date)).days)
$x.NewCerts = ($x.Group.newcert | ?{$_ -eq $true}).count
$x.OldCerts = ($x.Group.newcert | ?{$_ -eq $false}).count
}
# Return only info about subjects that found as enabled in Active-Directory. with the followed properties.
$valid = $groups | ?{$_.Enabled -eq $true}
return $valid | select Name, Count, NewCerts, OldCerts, LastLogOn, DaysFromLastLogOn, Type
}
<# Here you shall copy and past the codes of the certificate templates. you'll find it within your user/computer
certificate, in the 'Details' tab.
This code is neccessaty for the script, which uses it to filter and divert certificates only for user/computer,
and place it in the correct table. #>
$UserTemplate = "1.3.6.1.4.1.333.33.3.333333.333333.3333333.3333333.33333.3333.3333.33333336"
$CompTemplate = "1.3.6.1.4.1.311.21.8.33333333.33333.13333.333.333333.333.3333333.3333395"
# Call the function to get an array of all issued and valid certificates from your CA server.
$AllCerts = Get-Certs | ?{$_.Template -eq $UserTemplate -or $_.Template -eq $CompTemplate}
# Filter the list of all certificate to list of user certs, and list of computer certs.
$CertUsers = $AllCerts | ?{$_.Template -eq $UserTemplate}
$CertComputers = $AllCerts | ?{$_.Template -eq $CompTemplate}
# Call function to add extentions data of each cert into the table.
$ExtUsers = Insert-Info -list $CertUsers
$ExtComp = Insert-Info -list $CertComputers
# Call function to concetrate and process cert info related to a subject.
$GroupUserCerts = Group-Data -list $ExtUsers -type 'user'
$GroupCompCerts = Group-Data -list $ExtComp -type 'comp'
<# Export information to CSV files for user certs list, and the computer certs list.
For each exporting the general table of all certs - which focuses on the certs,
and also the final table with the groups - which focuses on the subject. #>
$ExtUsers | select Name, NewCert, DaysToExpiry, Expiry | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $env:USERPROFILE\Desktop\All-Cert-Users.csv
$ExtComp | select Name, NewCert, DaysToExpiry, Expiry |Export-Csv -NoTypeInformation -Encoding UTF8 -Path $env:USERPROFILE\Desktop\All-Cert-Comps.csv
$GroupUserCerts | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $env:USERPROFILE\Desktop\Cert-Users.csv
$GroupCompCerts | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $env:USERPROFILE\Desktop\Cert-Comps.csv
# Clean and shutdown the connection to the CA server and the CA DataBase.
$CertDB = $null
[GC]::Collect()