שחרור אוטומטי מבוקר של משתמשים נעולים

שחרור אוטומטי מבוקר של משתמשים נעולים

רקע:

"Brute Force Attack" או כח גס – בתרגום חופשי. זו אחת הדרכים הבסיסיות והוותיקות לפיצוח סיסמאות. ניסוי סדרתי של כל הצירופים האפשריים, עד שמקבלים את הצירוף הנכון.

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

Group Policy של מיקרוסופט מאפשר לשחרר את הנעילה באופן אוטומטי, לאחר פרק זמן מוגדר. כך גם הכלי המכונה Fine-Grained Password Policy. כלי אותו ניתן להגדיר באמצעות Active Directory Administrative Center, וכן הגדרות החיבור והאבטחה ב-Azure.

הבעיה:

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

הפתרון:

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

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

הערה:

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

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

הסקריפט:

# This function taking some data and send it by mail
function Table-ToMail {
    param ($data, $title, $receiver)


    # Form a table of the banned accounts.
    $style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
    $style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
    $style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
    $style = $style + "TD{border: 1px solid black; padding: 5px; }"
    $style = $style + "</style>"
    $body = ""
    $header = "<h2>" + $title + "</H2>"
    if ($data -ne $null){$body += $data | select name, samaccountname | ConvertTo-Html -Head $style -PreContent $header}


    # Configure mail sending.
    $messageParameters = @{    
        subject = $title
        body =$body | Out-String
        from =  "<alert@domain.suffix>" 
        to = $receiver 
        smtpserver ="smtp.server.domain.suffix"
    }


    # send mail if there is data to send.
    if ($body -ne ""){
        Send-MailMessage @messageParameters -BodyAsHtml -Encoding Unicode 
        Write-Host Mail Sent
    }
}


# Get Locked accounts from AD - only enabled accounts.
$locked = Search-ADAccount -LockedOut | ?{$_.enabled -eq $true}


# Get the old log from file - make sure isn't long than 10,000 raws.
$log = (Get-Content C:\log.log)[(-10000)..(-1)]
Clear-Content C:\log.log
$log | Add-Content C:\log.log


# Filter to get onnly raws that log unlock-account action.
$log = $log | ?{$_.indexof(";") -ne -1}


# Get record of accounts which are locked and should not get auto-unlocked.
$banned = Import-Csv C:\banned.csv
$banned = $banned | ?{$_.samaccountname -in $locked.samaccountname}


# Accounts which have been banned  on last check, and now got unlocked.
$cleared = $banned | ?{$_.samaccountname -notin $locked.samaccountname}


$bannednow = @()
$sysaccounts= @()


# Running through all locked accounts to sort and proccess it.
foreach ($user in $locked) {


    # Get the locked account from AD - including the groups which it's member-of.
    $y = Get-ADUser -Identity $user.samaccountname -Properties memberof | select name, samaccountname, memberof
    # From the groups, get the Type of the account.
    $y | Add-Member -Force -MemberType NoteProperty -Name "Group" -Value ($y.MemberOf | ?{$_.substring(0,8) -eq "CN=group"} | %{$_.split(",")[0].substring(3)}) 
    # Add date for the log.
    $y | Add-Member -Force -MemberType NoteProperty -Name "Time" -Value (Get-Date -Format "dd/MM/yyyy HH:mm")


    # Reffer to accounts that represent a persone.
    if ($y.Group -eq "group.a" -or $y.Group -eq "group.b" -or $y.Group -eq "group.c") {
        # Get from the log how many times the account had been unlocked in the last 30 minuts.
        $unlocks = $log | ?{$_.split(";")[1] -eq $y.name} | ?{((Get-Date) - ([datetime]::parseexact($_.split(";")[0], "dd/MM/yyyy HH:mm", $null))).Minutes -lt 30}
        
        # If the account already unlocked more 2 times in the last 30 minutes, or the account already in the Banned list.
        if ($unlocks.count -gt 1 -or $y.samaccountname -in $banned.samaccountname) {
            # If the account is not in the banned list.
            # This nested condition intended to make sure that in case the account already banned, do nothing. 
            # Action shall be done only if the account doesn't banned and locked more than 2 times in the last 30 minutes.
            if ($y.samaccountname -notin $banned.samaccountname) {
                $bannednow += $y | select Name, SamAccountName, Group                                           
            }
        } else {
            # If account is not banned and had not been locked more than 2 times in the last 30 minuts,
            # Unlock the account and log it to the log file.
            $user | Unlock-ADAccount 
            $y.Time.ToString() + ";" + $y.name + ";" + $y.Group + ";Auto-Unlocked"| Add-Content C:\log.log 
        }        
    } else {
        # Reffer to accounts that  NOT represent a persone. 
        $sysaccounts += $y | select Name, SamAccountName, Group 
    }
}


# Log the accounts that are added to the banned list.
if ($bannednow -ne $null) {
    "=====================================================" | Add-Content C:\log.log
    $y.Time.ToString() + "      " + "Banned Users: " | Add-Content C:\log.log 
    "=====================================================" | Add-Content C:\log.log
    foreach ($x in $bannednow) {$x.samaccountname | Add-Content C:\log.log}
    "=====================================================" | Add-Content C:\log.log 
}




# This accounts doesn't auto-unlocked, and added to the banned list, awaiting for someone to check the problem and unlock it. 
$banned += $bannednow
$banned += $sysaccounts | ?{$_.SamAccountName -notin $banned.SamAccountName}


# Log the accounts from Banned list, that had been unlocked since last check. 
if ($cleared -ne $null){
    "=====================================================" | Add-Content C:\log.log
    $y.Time.ToString() + "      " + "cleared Users: " | Add-Content C:\log.log 
    "=====================================================" | Add-Content C:\log.log
    foreach ($x in $cleared) {$x.samaccountname | Add-Content C:\log.log}
    "=====================================================" | Add-Content C:\log.log 
}


# Send data
Table-ToMail -data $banned -title "All Locked Accounts" -receiver "<system@domain.suffix>"
Table-ToMail -data $bannednow -title "Users Got Locked!" -receiver "<support@domain.suffix>"


# Export Banned list for next check.
$banned | Export-Csv -NoTypeInformation -Encoding UTF8 -Path C:\banned.csv.

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

הגדרה ופונקציות:
  1. הגדרת פונקציה ששולחת את המידע במייל. הפונקציה מקבלת 3 פרמטרים – משתנה המכיל את המידע, כותרת ונמען. הפונקציה מסדרת את נתוני המשתמשים שננעלו בטבלה, ושולחת את הטבלה לנמען שהוגדר.
  2. חיפוש ב- Active Directoryאחר כל המשתמשים הפעילים הנעולים כרגע, והשמה שלהם בתוך משתנה.
  3. איסוף המידע מכל ההרצות הקודמות שנאסף בקובץ לוג – אל תוך משתנה. מתוך הלוג שולפים את השורות האחרונות, עד 10 אלף שורות. לא יותר.
  4. איפוס קובץ הלוג והשמה מחדש של השורות שנאספו. כך מוודאים שהקובץ לא יכיל הרבה יותר מ-10 אלף שורות.
איסוף ומיון:
  1. מתוך המידע שלקחנו מקובץ הלוג, מיון שמשאיר בתוך המשתנה רק את השורות בהן יש מידע על שחרור נעילת משתמש.
  2. איסוף מקובץ CSV של רשימת המשתמשים שכבר היו נעולים מפעם קודמת, והשמה שלהם בתוך משתנה.
  3. מתוך אותו משתנה, השמה למשתנה חדש, של רשימת משתמשים שנחסמו בהרצה קודמת, ועדיין חסומים כרגע.
  4. השמה למשתנה נוסף של רשימת משתמשים שנחסמו בהרצה קודמת וכרגע אינם חסומים.
עיבוד נתונים:
  1. יצירת לולאה שעוברת על כל אחד מהחשבונות הנעולים שנמצאו בחיפוש הנוכחי ב-Active Directory.
  2. על כל אחד מהנעולים, שליפת אותו החשבון ב-Active Directory והשמת המאפיינים הבאים שלו אל תוך משתנה: שם, שם משתמש, רשימת קבוצות בהן הוא חבר.
  3. הוספת שם של קבוצה מסוימת בה חבר החשבון למאפיינים של המשתנה החדש. זה מועיל במקרה וכל החשבונות ב-Active Directory מתחלקים לשתי קבוצות (או יותר), שמגדירות אם מדובר במשתמש המייצג אדם, או משתמש המייצג משאב, שירות, אפליקציה וכו'.
    בהנחה שלקבוצות האלה ישנה תבנית מסוימת כגון "accounts-personal" מול "accounts-service", ניתן להגדיר בשורה 58 אחרי CN את המכנה המשותף של הקבוצות. זה אומר שדווקא את הקבוצה הזו שממיינת את סוג המשתמשים צריך לשמור. כך לדוגמה ניתן להוסיף אחרי CN= את accounts, ואז גם צריך לשנות את הערך ב-substring ל-11. שזה סכום התווים של CN= בתוספת שמונת התווים של המילה  Accounts.
  4. איסוף חותמת זמן בפורמט מסוים, והוספה שלה כמאפיין למשתנה שמכיל את פרטי החשבון שעליו עובדים כרגע.
  5. כעת מתבצע תנאי מקונן. אם הקבוצה שנשמרה בנקודה 11 (שורה 58 בסקריפט) תואמת לקבוצה שאומרת שזה משתמש אנושי:
    • א.     מתבצעת בדיקה בלוג, בהתאם לחותמת הזמן שנלקחה קודם. בודקים כמה רישומים נושאים חותמת זמן של מחצית השעה האחרונה ומספרים על שחרורי נעילות של החשבון הזה.
    • ב.      תנאי מקונן (בתוך התנאי הקודם) – עדיין בהנחה שמדובר בחשבון המייצג משתמש אנושי.
      אם נמצא יותר מתיעוד אחד של שחרור נעילה – כלומר – שני שחרורי נעילה. כלומר – זו כבר הנעילה השלישית בתוך 30 דקות, או שהחשבון כבר קיים ברשימת החסומים:
      • 1.     תנאי מקונן שלישי: אם החשבון איננו ברשימת החסומים – להוסיף לרשימת החשבונות שנחסמו כרגע.
    • ג.       אחרת – כלומר – החשבון לא ברשימת החסומים וגם לא ננעל יותר מפעמיים במחצית השעה האחרונה. אז משחררים את הנעילה ומתעדים שחרור נעילה בלוג יחד עם חותמת זמן.
  6. אחרת – כלומר – אם לא מדובר במשתמש אנושי, להוסיף לרשימת המשתמשים האפליקטיביים שננעלו עכשיו.
  7. עם תום פעולת הלולאה והמעבר על כל החשבונות הנעולים, ניתן להשתמש בתוצאות.

בחינת תוצאות:

  1. אם רשימת המשתמשים האנושיים שנחסמו עכשיו אינה ריקה – להוסיף ללוג תיעוד של הרשימה הזו.
  2. אל רשימת החסומים מן ההרצה הקודמת, נוספים אלה שנחסמו עכשיו – אנושיים ואפליקטיביים.
  3. אם רשימת המשתמשים שהשתחררו (מסעיף 8) בין ההרצה האחרונה לעכשיו – אינה ריקה. אז מוסיפים תיעוד ללוג של המשתמשים שיצאו מרשימת החסומים.
  4. קריאות לפונקציה לשלוח נתונים במייל – ניתן לשלוח במייל את הנתונים הבאים:
    • ·        כל החסומים נכון לעכשיו (מן הפעם הנוכחית בצירוף פעמים קודמות).
    • ·        כל החסומים מן ההרצה הנוכחית.
    • ·        כל החסומים האפליקטיביים.
    • ·        כל החסומים האנושיים.
      וכן על זו הדרך.
  5. יצוא לקובץ של רשימת החסומים המעודכנת.

סיכום:

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

אפשר להוסיף ולסווג בדקדקנות. להגדיר הפרדה בין משתמשים המריצים אפליקציות ושירותים למשתמשים המריצים task. ב-Group Policy אלה משתמשים המתחברים כ-Service או כ-Batch. כך אפשר לחסום את הראשונים מלבצע משימות מתוזמנות, ואת האחרונים מהתחברות כשירות.

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

כתיבת תגובה