- Identify bad password attempts and users targeted with AD FS event logs
- Protect future attacks by enabling Extranet Lockout Protection
- Provide additional recommendations to reduce future vulnerability by enabling passwordless solutions and disabling legacy authentication
AD FS Help Troubleshooting Mitigating Password Spray Attacks and Account Lockouts
Mitigating Password Spray Attacks and Account Lockouts
What does this guide do?
This workflow helps mitigate and prevent future password spray attacks, determine the cause of account lockouts, and set up lockout protection. Use this workflow if you want to set up Extranet Lockout, find the cause of a password spray attack, or find the cause of an account lockout.
Who is the target audience?
AD FS Admins and Support Engineers
How does it work?
Determine the cause of lockouts and prevent future lockouts.
What are you looking for help with?
This guide will walk you through the following to mitigate your problem:
What version of AD FS are you on?
Check Extranet Smart Lockout Values
View AD FS Lockout Activity Events
The event for AD FS extranet lockout is Event 1210. This event will appear for any extranet lockout activity, but does not differentiate between familiar and unfamiliar bad password attempts (for 2016 only). This event does not differentiate between familiar and unfamiliar bad password attempts.
Get-ADUser -id contosoUser-Properties UserPrincipalName, BadPwdCount, Lockedout
Get-AdfsAccountActivity -UserPrincipalName constosoUser@contoso.com
Verify your AD FS Configuration is Valid
Check the following settings on your AD FS servers:- EnableExtranetLockout: true
- ExtranetObservationWindow: The recommended value for ExtranetObservationWindow is 30 minutes.
- ExtranetLockoutThreshold
- Account Lockout Threshold: this setting is similar to the ExtranetLockoutThreshold setting in AD FS. It determines the number of failed logon attempts that will cause a user account to be locked out.
- Account Lockout Threshold: this setting determines for how long a user account is locked out. It´s reasonable to have this value configured with at least 10 minutes difference from ExtranetObservationWindow.
- Reset Account Lockout Counter After: this setting determines how much time must elapse from user's last logon failure before badPwdCount is reset to 0.
- ExtranetObservationWindow > Reset Account Lockout Counter
- ExtranetLockoutThreshold in AD FS < Account Lockout Threshold in AD
Check Events Associated with Lockout
Additional Event Information for Lockouts
- Event 4740: verify a user was locked out. Lockouts that come from the WAP server look like they are coming from the AD FS server.
- Event 411: Verifies client IPs and gives legacy authentication details
- Event 1203: Written on AD FS 2016 and later - extranet lockout behavior
- Event 512: This audit is written when AD FS allows a login attempt after extranet observation window has passed.
- Event 515: This audit is written when there is a successful login after extranet observation windows has passed.
- Event 516: The following account has been locked out due to too many bad password attempts Comment: This audit is written when AD FS receives a login attempt and the user account has been locked out. This will happen for the first time when the account is locked out or after the account is locked out and extranet observation window has not passed. Comment: includes activity ID that can be correlated with same field name in Event 403
- Event 403: signals an HTTP request was received, includes activity ID that can be correlated with same field name in Event 516
- 4624: Successful authentication event
- Event 4625: An account failed to log on. Includes Security ID, Account Name + Account Domain fields. Bad logon after the account has been locked out. Existence of this event is a good clue that an attacker exists. Prior to exceeding account lockout threshold, you see logon failure. On lockout, you see failure reason field to distinguish failure reason. Status: 0xc000006D / unknown username or bad password Sub Status 0xc000006A = account locked out.
Manage User Account Activity with ESL
Use the following commands to reset the lockout counter for the user account in question.
-
Reset-ADFSAccountLockout
Resets the lockout counter for a user account
Reset-ADFSAccountLockout user@contoso.com -Familiar
Update the account activity for the user account. Add new familiar locations or erase state for any account.
-
Set-ADFSAccountActivity
Update the account activity for a user account. This can be used to add new familiar locations or erase state for any account.
Set-ADFSAccountActivity user@contoso.com -FamiliarLocation "1.2.3.4"
Are you seeing a large amount of event 222 on your WAP servers?
Are you seeing a large amount of event 222 on your WAP servers?
- Disable congestion control by following the steps in https://msdn.microsoft.com/en-us/library/azure/dn528859.aspx
- On your Web Application Proxy computer, start an elevated command window.
- Navigate to the AD FS directory, at %WINDIR%\adfs\config.
- Change the congestion control settings from its default values to ‘<congestionControl latencyThresholdInMSec="8000" minCongestionWindowSize="64" enabled="false" />’.
- Save and close the file.
- For versions under 2019: Restart the AD FS service by running ‘net stop adfssrv’ and then ‘net start adfssrv’.
Give us Feedback to Improve our Guide
What version of AD FS are you on?
Use Event Logs to Find Affected Users
cls
if ($PastHours -gt 0)
{
$PastPeriod = (Get-Date).AddHours(-($PastHours))
}
else
{$PastPeriod = $PastDays}
$CS = get-wmiobject -class win32_computersystem
$Hostname = $CS.Name + '.' + $CS.Domain
$Instances = @()
Get-Winevent -ComputerName $Hostname -LogName Security | Where-Object {(($_.ID -eq 501) `
-and ($_.Properties.Value -contains $SearchCriteria) -and ($_.TimeCreated -gt $PastPeriod))} | % { $Instances += $_.Properties[0].Value}
function FindADFSAuditEvents {
param ($valuetomatch, $counter, $instance, $PastPeriod)
$Results = $pwd.Path + "\" + $SearchCriteria + "-ADFSSecAudit" + '-' + $Counter + ".txt"
$SearchString = $SearchCriteria + " and instance " + $Instance + " in Security event log."
"Security Audit Events which match $SearchString" | Out-File $Results -Encoding UTF8
Get-WinEvent -ComputerName $Hostname -LogName Security -WarningAction SilentlyContinue | `
Where-Object -ErrorAction SilentlyContinue {($_.TimeCreated -gt $PastPeriod) -and (($_.Properties -contains $ValueToMatch) -or ($_.Properties[0].Value -match $Instance))} | % {
$Event = New-object PSObject
add-member -inputobject $Event -membertype noteproperty -name "Event ID" -value $_.ID
add-member -inputobject $Event -membertype noteproperty -name "Provider" -value $_.ProviderName
add-member -inputobject $Event -membertype noteproperty -name "Machine Name" -value $_.MachineName
add-member -inputobject $Event -membertype noteproperty -name "User ID" -value $_.UserID
add-member -inputobject $Event -membertype noteproperty -name "Time Created " -value $_.TimeCreated
$Event | FL *
$Event | Out-File $Results -Encoding UTF8 -Append
$_.Properties | FL *
$_.Properties | Out-File $Results -Encoding UTF8 -Append
$DateTimeExport = $_.TimeCreated
}
$DateTime = (($DateTimeExport.ToShortDateString()).Replace('/','-') + '@' + (($DateTimeExport.ToShortTimeString()).Replace(' ','')))
$DateTime = $DateTime.Replace(':','')
$Results2 = $pwd.Path + "\" + $SearchCriteria + '-' + $DateTime + "-ADFSSecAudit" + $Counter + ".txt"
Rename-Item -Path $Results -NewName $Results2
}
$Counter = 1
foreach ($instance in $Instances)
{
FindADFSAuditEvents -ValueToMatch $SearchCriteria -Instance $Instance -PastPeriod $PastPeriod -Counter $Counter
$Counter++
}
For web based and most application authentication scenarios the malicious IP will be in the x-ms-client-ip field.
For non-Modern Authentication Outlook clients, the IP address of the malicious submitter will be in the x-ms-forwarded-client-ip
and Microsoft Exchange Online server IPs will be in the x-ms-client-ip value.
This is a result of "legacy" or Basic authentication having the Exchange Online servers in the cloud proxying the authentication verification on behalf of the Outlook client. Mail clients which support Modern Authentication (aka ADAL) will not proxy the auth this way.
Compare the IP addresses in the Excel document to the list of Microsoft Exchange IP addresses
What version of AD FS are you on?
The best way to mitigate future problems is by upgrading to AD FS 2012R2+
AD FS 2016 has an Extranet Smart Lockout feature that is the best way to protect users from password spray attacks.
Advantages of Enabling Extranet Lockout
- AD FS protects your users from brute force attacks by locking out the malicious user from extranet access where an attacker tries to guess a password by continously sending authentication requests
- The malicious user account will be locked out for extranet access
- Protects user accounts from malicious account lockout when an attacker wants to lock out a user account by sending authentication requests with wrong passwords.
- While the malicious user will be locked out of extranet access, the actual user account in AD is not locked out and the user can still access corporate resources within the organization.
Information on Upgrading to AD FS
Please upgrade to 2012R2 or higher.
Our official recommendation is upgrading to a higher version to be able to mitigate your problem.
Please return to this troubleshooting guide for help on how to set up Extranet Lockout protection once you have upgraded.
Check if Connect Health is Available
Use Connect Health to Generate Data on User Login Activity
Follow these steps to view the Connect Health dashboard in the Azure Portal.
- Log into Azure as a Tenant Administrator
- Click Azure Active Directory
- Click Azure AD Connect
- Click the Azure AD Connect Health link in the Health and Analytics Section
- Click the Pin at the top and Pin this to your Dashboard!
- Click your AD FS link
View the total number of failed login attempts due to invalid username or password in the last 30 days and the average # of users that fail with a bad username/password login per day.
Use this report to detect the IP addresses that exceed a threshold of failed based logins, and detect if the number of attempts per user may be under the threshold for account protection in AD FS.
For more information, visit Using Connect Health with AD FS.
Compare the IP addresses from Connect Health reports to the list of Microsoft Exchange IP addressesAnalyze the IP and user name of accounts that are affected by bad password attempts
Consider Enabling Connect Health
Check if Audit Logs are Enabled
- Click Start, point to Administrative Tools, and then click Local Security Policy.
- Navigate to the Security Settings\Local Policies\User Rights Assignment folder, and then double-click Generate security audits.
- Double-click Local Policies, and then click Audit Policy.
- In the details pane, double-click Audit object access.
- Check the status of the audit object policy on the Audit object access Properties page.
Enable Auditing in AD FS
Steps to Enable Auditing
- From the primary AD FS server, open the AD FS management console.
- On the Action menu, click Edit Federation Service properties.
- In Federation Service Properties, click the Events tab.
- Check all of the available checkboxes. This will enable all AD FS event messages and audit logging.
Is Extranet Lockout enabled?
Ensure Best Practices
Check the following settings on your AD FS servers:
- EnableExtranetLockout: true
- ExtranetObservationWindow: The recommended value for ExtranetObservationWindow is 30 minutes.
- ExtranetLockoutThreshold
Check the following settings on your AD:
- Account Lockout Threshold: this setting is similar to the ExtranetLockoutThreshold setting in AD FS. It determines the number of failed logon attempts that will cause a user account to be locked out.
- Account Lockout Threshold: this setting determines for how long a user account is locked out. It´s reasonable to have this value configured with at least 10 minutes difference from ExtranetObservationWindow.
- Reset Account Lockout Counter After: this setting determines how much time must elapse from user's last logon failure before badPwdCount is reset to 0.
Make sure the following is valid. If it is invalid, your Extranet Lockout has not been functioning correctly.
- ExtranetObservationWindow > Reset Account Lockout Counter
- ExtranetLockoutThreshold in AD FS < Account Lockout Threshold in AD
Analyze the IP and user name of accounts that are affected by bad password attempts
Identify Targeted User IPs
- You can download the PowerShell script to search your AD FS servers for events 411 at this link. The script will provide a CSV file which contains the UserPrincipalName, IP address of submitter, and time of all bad credential submissions to your AD FS farm.
- You can open the CSV in Excel and quickly filter by username, or IP or times.
PARAM ($PastDays = 1, $PastHours)
#************************************************
# ADFSBadCredsSearch.ps1
# Version 1.0
# Date: 6-20-2016
# Author: Tim Springston [MSFT]
# Description: This script will parse the AD FS server's (not proxy) security AD FS
# for events which indicate an incorrectly entered username or password. The script can specify a
# past period to search the log for and it defaults to the past 24 hours. Results will be placed into a CSV for
# review of UPN, IP address of submitter, and timestamp.
#************************************************
cls
if ($PastHours -gt 0)
{$PastPeriod = (Get-Date).AddHours(-($PastHours))}
else
{$PastPeriod = (Get-Date).AddDays(-($PastDays)) }
$Outputfile = $Pwd.path + "\BadCredAttempts.csv"
$CS = get-wmiobject -class win32_computersystem
$Hostname = $CS.Name + '.' + $CS.Domain
$Instances = @{}
$OSVersion = gwmi win32_operatingsystem
[int]$BN = $OSVersion.Buildnumber
if ($BN -lt 9200){$ADFSLogName = "AD FS 2.0/Admin"}
else {$ADFSLogName = "AD FS/Admin"}
$Users = @()
$IPAddresses = @()
$Times = @()
$AllInstances = @()
Write-Host "Searching event log for bad credential events..."
if ($BN -ge 9200) {Get-Winevent -FilterHashTable @{LogName= "Security"; StartTime=$PastPeriod; ID=411} -ErrorAction SilentlyContinue | Where-Object {$_.Message -match "The user name or password is incorrect"} | % {
$Instance = New-Object PSObject
$UPN = $_.Properties[2].Value
$UPN = $UPN.Split("-")[0]
$IPAddress = $_.Properties[4].Value
$Users += $UPN
$IPAddresses += $IPAddress
$Times += $_.TimeCreated
add-member -inputobject $Instance -membertype noteproperty -name "UserPrincipalName" -value $UPN
add-member -inputobject $Instance -membertype noteproperty -name "IP Address" -value $IPAddress
add-member -inputobject $Instance -membertype noteproperty -name "Time" -value ($_.TimeCreated).ToString()
$AllInstances += $Instance
$Instance = $null
}
}
$AllInstances | select * | Export-Csv -Path $Outputfile -append -force -NoTypeInformation
Write-Host "Data collection finished. The output file can be found at $outputfile`."
$AllInstances = $null
More information on the 411 events themselves:
- These events will contain the user principal name (UPN) of the targeted user.
- These events will also contain a message "token validation failed" and will say if it was a bad password attempt or the account is locked out.
Are the IPs identified external IPs (i.e. coming from a different country/region vs organization IPs)
Are IP addresses coming from Exchange Online?
Block IPs on Exchange Online
Supported formats
The following IP address formats are supported:- Standard IPv4 and IPv6 addresses
- IP ranges
- CIDR format- 2001:0DB8::CD3/60
- High-Low format- 192.168.0.1-192.168.0.254
- Sub masking format- 192.168.8.2(255.255.255.0)
Instructions to Block IPs
- If you haven’t already done so on the PS client being used, run the following: Set-ExecutionPolicy RemoteSigned
- Connect to EXO using steps 1-3 in the link below with Global Admin or Exchange admin security context: https://technet.microsoft.com/en-us/library/jj984289(v=exchg.160).aspx
- If you have enabled MFA for your Administrator, please follow the steps in the document to get connect to EXO PowerShell: https://technet.microsoft.com/en-us/library/mt775114(v=exchg.160).aspx
- Enable the block list for the IP address assigned to the test device used in step 1
Set-OrganizationConfig <tenant name="">.onmicrosoft.com -IPListBlocked 127.0.0.2
Set-OrganizationConfig <tenant name="">.onmicrosoft.com -IPListBlocked@{add="198.76.9.23", "172.16.0.0-172.31.255.255","2001:db8:0:1234:0:567:8:1", "2001:0DB8::CD3/60","192.168.8.2(255.255.255.0)","2001:db8::1","2001:0DB8:0000:CD30:0000:0000:0000:0000/60","ABCD:EF01:2345:6789:ABCD:EF01:2345:6789"}
Set-OrganizationConfig -Identity <tenant name="">
-IPListBlocked @{remove="0.0.0.0"}
Verify your block list submission
get-OrganizationConfig <tenant name="">.onmicrosoft.com | select -ExpandProperty IPListBlocked
2001:0DB8:0000:CD30:0000:0000:0000:0000/60
2001:db8::1192.168.8.2(255.255.255.0)
2001:0DB8::CD3/60
2001:db8:0:1234:0:567:8:1
172.16.0.0-172.31.255.255
198.76.9.23
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
127.0.0.2
Enable Extranet Lockout
If you have external IPs that you know are not coming from your users, block them
If you have external IPs that you know are not coming from your users, block them
Configure Extranet Lockout
Advantages of Extranet Lockout
- Protects your user accounts from brute force attacks where an attacker tries to guess a user's password by continuously sending authentication requests.
- The user account will be locked out by AD FS for extranet access, the actual user account in AD is not locked out and the user can still access corporate resources within the organization
Steps to Configure
There are 3 settings in AD FS that you need to configure to enable this feature:- EnableExtranetLockout : set this Boolean value to be True if you want to enable Extranet Lockout.
- ExtranetLockoutThreshold : Integer defining maximum number of bad password attempts. Once this threshold is reached, AD FS will immediately reject the request from the extranet without attempting to contact the domain controller for authentication, no matter if password is good or bad. This means the value of badPwdCount attribute of an AD account will not increase while the account is soft-locked out.
- ExtranetObservationWindow : Timespan determining how long the user account will be soft locked out. AD FS will start to perform username and password authentication again when the window is passed. AD FS uses the AD attribute badPasswordTime as the reference for determining whether the extranet observation window has passed or not. The window has passed if current time > badPasswordTime + ExtranetObservationWindow.
Set-AdfsProperties -EnableExtranetLockout $true -ExtranetLockoutThreshold 15 -ExtranetObservationWindow (new-timespan -Minutes 30)
Extranet Lockout and AD Lockout Policy
The Extranet Lockout feature in AD FS works independently from the AD lockout policy. Check the AD lockout policy settings. There are three settings to check.- Account Lockout Threshold: this setting is similar to the ExtranetLockoutThreshold setting in AD FS. It determines the number of failed logon attempts that will cause a user account to be locked out. In order to protect your user accounts from a malicious account lockout attack, you want to set the value of ExtranetLockoutThreshold in AD FS less than the Account Lockout Threshold value in AD
- Account Lockout Duration: this setting determines for how long a user account is locked out. This setting does not matter much in this conversation as Extranet Lockout should always happen before AD lockout happens if configured properly
- Reset Account Lockout Counter After: this setting determines how much time must elapse from user's last logon failure before badPwdCount is reset to 0. In order for Extranet Lockout feature in AD FS to work well with AD lockout policy, you want to make sure the value of ExtranetObservationWindow in AD FS > the Reset Account Lockout Counter After value in AD. The examples below will explain why.
Consider Passwordless Authentication
Set Azure MFA as Primary Auth on AD FS 2016
Set Azure MFA as Primary Auth on AD FS 2016 Using Azure MFA as the primary authentication for AD FS protects password based sign-in by requiring an additional factor, such as a verification code, prior to the user entering the password. Compromising MFA presents a significant challenge for attackers, even if they have access to the user’s password, as they need the additional authentication method. Additional information and instructions on Configuring AD FS 2016 and Azure MFA can be found here: Configure AD FS and Azure MFA If you do not wish to use MFA as a primary authentication, consider enabling it as an additional second factor authentication method.
Set Conditional Access Policies
Implementing conditional access can help you manage which users will be allowed to see AD FS -secured resources. AD FS CA policies can be managed by modifying Authorization claims rules, which can be enforced based on user identity or group membership, network location, authentication state, and device (i.e. workplace joined). Additional resources for setting up CA policies are found at Managing Risk with Conditional Access Control and Setup Walkthrough of managing risk with Conditional Access.
Enable Certificate Based Authentication
Enable Windows Hello for Business
Is the user account used as a service account?
Ensure credentials are updated in the service
Are user creds cached in the application?
Clear cache and update the credentials in the application
Check if Connect Health is Available
Use Connect Health to Generate Data on Userss
Follow these steps to view the Connect Health dashboard in the Azure Portal.
- Log into Azure as a Tenant Administrator
- Click Azure Active Directory
- Click Azure AD Connect
- Click the Azure AD Connect Health link in the Health and Analytics Section
- Click the Pin at the top and Pin this to your Dashboard!
- Click your AD FS link
View the total number of failed login attempts due to invalid username or password in the last 30 days and the average # of users that fail with a bad username/password login per day.
Use this report to detect the IP addresses that exceed a threshold of failed based logins, and detect if the number of attempts per user may be under the threshold for account protection in AD FS.
For more information, visit Using Connect Health with AD FS.
Analyze the IP and user name of accounts that are affected by bad password attempts
Consider Enabling Connect Health
Advantages of Connect Health
- Reports for AD FS such as top 50 users who have bad username/password attempts and their last IP address
- Risky IP report for failed AD FS sign-ins
- Trends in performance data, usage analytics for sign ins, monitoring with alerts for health of servers
- Monitoring with alerts to know when AD FS and AD FS proxy servers are not healthy
Check if Audit Logs are Enabled
- Click Start, point to Administrative Tools, and then click Local Security Policy.
- Navigate to the Security Settings\Local Policies\User Rights Management folder, and then double-click Generate security audits.
- Double-click Local Policies, and then click Audit Policy.
- In the details pane, double-click Audit object access.
- Check the status of the audit object policy on the Audit object access Properties page.
Enable Auditing in AD FS
Steps to Enable Auditing
- From the primary AD FS server, open the AD FS management console.
- On the Action menu, click Edit Federation Service properties.
- In Federation Service Properties, click the Events tab.
- Check all of the available checkboxes. This will enable all AD FS event messages and audit logging.
Set-AdfsProperties -AuditLevel Verbose
Gather information on User Lockouts
# ---------------------------------
# Event 411 Reader v2 by MingChen@Microsoft.com 6/19/2017
# This script will scan saved AD FS security evtx for 411 token validation failed (and 1203) events, extract DateTime, Account, Error, internal and external IPAddresses into CSV and Excel.
# Feel free to modify to fit your need.
# ---------------------------------
# Script requires latest Excel (Recommend 64 bits to allow larger dataset prcessing.)
# To use this script
# 1. Enable AD FS auditing, save resulting AD FS security log(s). https://technet.microsoft.com/en-us/library/cc738766(v=ws.10).aspx#BKMK_97
# 2. Put this script and evtx(s) in same directory, follow screen prompt for manual path to Evtx and FileName.
# 3. This script will (1)Read event 411 (and 1203 if needed) from *.evtx, (2)If needed, correlate 411 matching 1203 on TimeCreated for IP info. (3) Export 411-*.csv, and, (4) import/append/customize Excel for later analysis.
#
# Change log:
# - V2, changed CSV export code to write all events at once to speed up export.
# - Added var to change behaviors of the script
# - $mcScriptPrompts > when set to $false, script will process all evtx in the script directory without prompting. Good for dropping evtx, then right-click script to "Run with PowerShell"
# - $mcMaxExport > max events to export per EVTX, set to low number for quick sample/training.
# - $mcMaxImport > max events to import into excel, script will finish current file and stop importing next file(s). Set to lower number if Excel is returning memory error during import.
# - $mcStartTime > Earliest 411 event to export, in the form of 'yyyy/MM/dd HH:mm', where HH 13 is 1pm, example: '2017/05/26 13:01'. Use this to gage progress of account lockouts after blocked IP.
# - $mcMaxThreads > max number of evtx files to export at the same time. 4 default, set to 8 and experiment if you have fast SSD HDD.
#
# Script variables block, modify to fit your need ---------------------------------------------------------------------
$mcScriptPrompts = $true #Script default, $false for non-interactive input and output.
$mcMaxExport = 50000 #50000 Max events to export per each EVTX, modify this if we are getting Excel memory error (or we just need first few evt for samples)
$mcMaxImport = 50000 #50000 max events to import into excel, script will finish current csv file and stop importing next file(s).
$mcStartTime = '2016/05/25 12:00' #Earliest 411 event to export, in the form of 'yyyy/MM/dd HH:mm', where HH 13 is 1pm, example: '2017/05/26 13:01' Use this to gage progress of account lockouts after blocked IP.
$mcMaxThreads = 4 #Max concurrent export threads running at the same time.
# Internal values, do not modify ---------------------------------------------------------------------------------------
$mcCSVImport = $true
$mcEvtPath = $null
$MultiIP2 = $SingleIP1 = $SingleIP2 = $false
$mcStartTime=[datetime]::ParseExact($mcStartTime,'yyyy/MM/dd HH:mm',$null) #Convert string to timestamp
#----Functions--------------------------
Function mcCleanUpExcelObj { #Clear out Excel ComObj at the end from https://theolddogscriptingblog.wordpress.com/2010/06/01/powershell-excel-cookbook-ver-2/
[Management.Automation.ScopedItemOptions]$scopedOpt = 'ReadOnly, Constant'
Get-Variable -Scope 1 | Where-Object {
$_.Value.pstypenames -contains 'System.__ComObject' -and -not ($scopedOpt -band $_.Options)
} | Remove-Variable -Scope 1 -Verbose:([Bool]$PSBoundParameters['Verbose'].IsPresent)
[gc]::Collect()
}
Function mcSetPivotField($mcPivotFieldSetting) { #Set pivot field attributes per MSDN https://msdn.microsoft.com/en-us/library/office/ff820762.aspx
if ($mcPivotFieldSetting[1] -ne $null) { $mcPivotFieldSetting[0].Orientation = $mcPivotFieldSetting[1]} # 1 Orientation { $xlRowField | $xlDataField |$xlColumnField }, in XlPivotFieldOrientation
if ($mcPivotFieldSetting[2] -ne $null) { $mcPivotFieldSetting[0].NumberFormat = $mcPivotFieldSetting[2]} # 2 NumberFormat { $mcNumberF | $mcPercentF }
if ($mcPivotFieldSetting[3] -ne $null) { $mcPivotFieldSetting[0].Function = $mcPivotFieldSetting[3]} # 3 Function { $xlAverage | $xlSum | $xlCount }, in XlConsolidationFunction
if ($mcPivotFieldSetting[4] -ne $null) { $mcPivotFieldSetting[0].Calculation = $mcPivotFieldSetting[4]} # 4 Calculation { $xlPercentOfTotal | $xlPercentRunningTotal }, in XlPivotFieldCalculation
if ($mcPivotFieldSetting[5] -ne $null) { $mcPivotFieldSetting[0].BaseField = $mcPivotFieldSetting[5]} # 5 BaseField <String>
if ($mcPivotFieldSetting[6] -ne $null) { $mcPivotFieldSetting[0].Name = $mcPivotFieldSetting[6]} # 6 Name <String>
if ($mcPivotFieldSetting[7] -ne $null) { $mcPivotFieldSetting[0].Position = $mcPivotFieldSetting[7]} # 7 Position
}
Function mcSetPivotTableFormat($mcPivotTable) { # Set pivotTable cosmetics and sheet name
$mcPT=$mcPivotTable[0].PivotTables($mcPivotTable[1])
$mcPT.HasAutoFormat = $False #2.turn of AutoColumnWidth
for ($i=2; $i -lt 9; $i++) { #3. SetColumnWidth for Sheet($mcPivotTable[0]),PivotTable($mcPivotTable[1]),Column($mcPivotTable[2-8])
if ($mcPivotTable[$i] -ne $null) { $mcPivotTable[0].columns.item(($i-1)).columnWidth = $mcPivotTable[$i]}
}
$mcPivotTable[0].Application.ActiveWindow.SplitRow = 3
$mcPivotTable[0].Application.ActiveWindow.SplitColumn = 2
$mcPivotTable[0].Application.ActiveWindow.FreezePanes = $true #1.Freeze R1C1
$mcPivotTable[0].Cells.Item(1,1)="Filter"
$mcPivotTable[0].Cells.Item(3,1)=$mcPivotTable[9] #4 set TXT at R3C1 with PivotTableName$mcPivotTable[9]
$mcPivotTable[0].Name=$mcPivotTable[10] #5 Set Sheet Name to $mcPivotTable[10]
$mcRC = ($mcPivotTable[0].UsedRange.Cells).Rows.Count-1
if ($mcPivotTable[11] -ne $null) { # $mcPivotTable[11] Set ColorScale
$mColorScaleRange='$'+$mcPivotTable[11]+'$4:$'+$mcPivotTable[11]+'$'+$mcRC
[Void]$mcPivotTable[0].Range($mColorScaleRange).FormatConditions.AddColorScale(3) #$mcPivotTable[11]=ColorScale
$mcPivotTable[0].Range($mColorScaleRange).FormatConditions.item(1).ColorScaleCriteria.item(1).type = 1 #xlConditionValueLowestValue
$mcPivotTable[0].Range($mColorScaleRange).FormatConditions.item(1).ColorScaleCriteria.item(1).FormatColor.Color = 8109667
$mcPivotTable[0].Range($mColorScaleRange).FormatConditions.item(1).ColorScaleCriteria.item(2).FormatColor.Color = 8711167
$mcPivotTable[0].Range($mColorScaleRange).FormatConditions.item(1).ColorScaleCriteria.item(3).type = 2 #xlConditionValueHighestValue
$mcPivotTable[0].Range($mColorScaleRange).FormatConditions.item(1).ColorScaleCriteria.item(3).FormatColor.Color = 7039480
}
if ($mcPivotTable[12] -ne $null) { # $mcPivotTable[12] Set DataBar
$mcDataBarRange='$'+$mcPivotTable[12]+'$4:$'+$mcPivotTable[12]+'$'+$mcRC
[void]$mcPivotTable[0].Range($mcDataBarRange).FormatConditions.AddDatabar() #$mcPivotTable[12]:Set DataBar
}
}
Function mcSortPivotFields($mcPF) { #Sort on $mcPF and collapse later pivot fields
for ($i=2; $i -lt 5; $i++) { #collapse later pivot fields
if ($mcPF[$i] -ne $null) {
$mcPF[$i].showDetail = $false
}
}
[void]($mcPF[0].Cells.Item(4, 2)).sort(($mcPF[0].Cells.Item(4, 2)), 2)
}
Function mcSetPivotTableHeaderColor($mcSheet) { #Set PiviotTable Header Color for easier reading
for ($i=1; $i -lt 5; $i++) { #Set header(s) color
if ($mcSheet[$i] -ne $null) { $mcSheet[0].Range(($mcSheet[$i]+"3")).interior.Colorindex = 37 }
}
}
#----Main---------
$mcScriptPath = Split-Path ((Get-Variable MyInvocation -Scope 0).Value).MyCommand.Path
if ($mcScriptPrompts) { #Interactive mcScriptPrompts
$mcEvtPath = Read-Host "Enter local, mapped or UNC path to evtx or previously generated csv. Be sure to remove trailing blank. For Example (c:\CaseData)`n Or press [Enter] if script is in the evtx or csv folder.`n"
if ($mcEvtPath -eq '') { #If there is no Path entered, we will use the same directory as script path.
$mcEvtPath= $mcScriptPath
Write-Host "No path entered, default to " $mcEvtPath
}
} else {
$mcEvtPath= $mcScriptPath #Non-interactive script uses script path for Event path
}
Write-Host 'Convert evtx to csv.'
$mcFiles = Get-ChildItem -Path $mcEvtPath | Where {$_.name -clike '*.evtx'}
ForEach ($mcFile in $mcFiles) { #Loop through *.evtx
$mcFile | Add-Member -MemberType NoteProperty -Name MaxEvents -force -Value $mcMaxExport
$mcFile | Add-Member -MemberType NoteProperty -Name StartTime -force -Value $mcStartTime
#Job [ #Job start code
Start-Job -ArgumentList (,$mcFile) -ScriptBlock { #Start-Job for reading each files
param ($mcFile)
#Job ]
Write-Host ('Reading ',$mcFile.FullName)
$mc411s = Get-WinEvent -FilterHashtable @{Path=$mcfile.FullName; LogName="AD FS Auditing"; Level=0; StartTime=$mcFile.StartTime; id="411" } -MaxEvents $mcFile.MaxEvents -ErrorAction SilentlyContinue
If ($mc411s -ne $null) {
$mcOutFile = $mcFile.DirectoryName+'\411-'+$mcFile.Name+'.csv'
Write-Host (' Event 411 found, generating', $mcOutFile)
If ( $mc411s[0].Properties[4].count -eq 0) { #check if 411 contain IPAddresses, if not, read event 1203 to extract IP
$mc1203s = Get-WinEvent -FilterHashtable @{Path=$mcfile.FullName; LogName="AD FS Auditing"; Level=0; StartTime=$mcFile.StartTime; id="1203" } -MaxEvents $mcFile.MaxEvents -ErrorAction SilentlyContinue
}
$mc411sOut = @() #Array for exporting CSV items
ForEach ($mc411 in $mc411s) {
$Account = $($mc411.Properties[2].value).Split("-")
if ($mc411.Properties[4].count -ne 0) { # Check if 411 contains IPAddresses
$mcObject = New-Object System.Object
$IPAddresses = $mc411.Properties[4].value.Split(',')
$mcObject | Add-Member -MemberType NoteProperty -Name Server -force -Value $mc411.MachineName
$mcObject | Add-Member -MemberType NoteProperty -Name DateTime -force -Value $mc411.TimeCreated
$mcObject | Add-Member -MemberType NoteProperty -Name Account -force -Value $Account[0]
$mcObject | Add-Member -MemberType NoteProperty -Name Error -force -Value $Account[1]
$mcObject | Add-Member -MemberType NoteProperty -Name IPAddress1 -force -Value $IpAddresses[0]
$mcObject.Server = $mcObject.Server.Trim()
$mcObject.Account = $mcObject.Account.Trim()
$mcObject.IPAddress1 = $mcObject.IPAddress1.Trim()
If ($IPAddresses.Count -eq 2) {
$MultiIP2 = $true
$mcObject | Add-Member -MemberType NoteProperty -Name IPAddress2 -force -Value $IpAddresses[1]
$mcObject.IPAddress2 = $mcObject.IPAddress2.Trim()
$mc411sOut += $mcObject
} else {
$mcObject | Add-Member -MemberType NoteProperty -Name IPAddress2 -force -Value $null
$mc411sOut += $mcObject
}
} else { #411 does not contain IP addresses, need to get them from 1203 event with the same timestamp
ForEach ($mc1203 in $mc1203s) {
If ($mc411.TimeCreated -eq $mc1203.TimeCreated) {
$mcObject = New-Object System.Object
$IPAddresses = $(Select-Xml -Content $mc1203.Properties[1].value -XPath "//IpAddress").Node.InnerText.Split(',')
$mcObject | Add-Member -MemberType NoteProperty -Name Server -force -Value $mc411.MachineName
$mcObject | Add-Member -MemberType NoteProperty -Name DateTime -force -Value $mc411.TimeCreated
$mcObject | Add-Member -MemberType NoteProperty -Name Account -force -Value $Account[0]
$mcObject | Add-Member -MemberType NoteProperty -Name Error -force -Value $Account[1]
$mcObject | Add-Member -MemberType NoteProperty -Name IPAddress1 -force -Value $IpAddresses[0]
$mcObject.Server = $mcObject.Server.Trim()
$mcObject.Account = $mcObject.Account.Trim()
$mcObject.IPAddress1 = $mcObject.IPAddress1.Trim()
If ($IPAddresses.Count -eq 2) {
$MultiIP2 = $true
$mcObject | Add-Member -MemberType NoteProperty -Name IPAddress2 -force -Value $IpAddresses[1]
$mcObject.IPAddress2 = $mcObject.IPAddress2.Trim()
$mc411sOut += $mcObject
} else {
$mcObject | Add-Member -MemberType NoteProperty -Name IPAddress2 -force -Value $null
$mc411sOut += $mcObject
}
}
}
}
}
$mc411sout | ConvertTo-Csv -NoTypeInformation | Out-File $mcOutFile
}
#Job [ # Maxthread code
} | Out-Null #Job output
write-host 'Processing: ' $mcFile.name
While((Get-Job -State 'Running').Count -ge $mcMaxThreads) { Start-Sleep -Milliseconds 10 } # Pause, only run $mcMaxThreads number of jobs.
#Job ]
}
#Job [ # Job clean out code
While((Get-Job -State 'Running').Count -gt 0) { Start-Sleep -Milliseconds 10 } # wait for last job to complete
#Get-Job | Receive-Job
Get-Job -State Completed | Remove-Job
#Job ]
#----Import csv to excel-----------------------------------------------------
if ($mcCSVImport -eq $true) { #Debug only, perforem CSV to Excel import
$mcFiles = Get-ChildItem -Path $mcEvtPath | Where {$_.name -clike '411-*.csv'}
If ($mcFiles -ne $null) { #Create a new Excel workbook if there are CSV in directory.
cd $mcEvtPath #for Import-Csv to read all files.
$mcObject = Import-Csv $mcfiles -Delimiter ','
cd $mcScriptPath #Back to script path
$MultiIP2 = ($mcObject | Where-Object {$_.IPAddress2 -ne ''}).count -ne 0 #check for IpAddress2 in CSV in case this is a CSV only re-run.
$SingleIP1 = ((($mcObject | Where-Object {$_.IPAddress1 -ne ''}).Count -eq $null) -and (($mcObject | Where-Object {$_.IPAddress1 -ne ''}).count -ne 0)) #Check for single IP1 address for proper time pivot grouping.
$SingleIP2 = ((($mcObject | Where-Object {$_.IPAddress2 -ne ''}).Count -eq $null) -and (($mcObject | Where-Object {$_.IPAddress2 -ne ''}).count -ne 0)) #Check for single IP2 address for proper time pivot grouping.
Write-Host 'Import csv to excel.'
$mcExcel = New-Object -ComObject excel.application
$mcWorkbooks = $mcExcel.Workbooks.Add()
$Sheet1 = $mcWorkbooks.worksheets.Item(1)
$mcCurrentRow = 1
ForEach ($mcFile in $mcFiles) { #Define Excel TXT connector and import/append
if ($mcCurrentRow -le $mcMaxImport){ #Import only up to $mcMaxImport number of records.
$mcConnector = $Sheet1.QueryTables.add(("TEXT;" + $mcEvtPath+'\'+$mcFile),$Sheet1.Range(('a'+($mcCurrentRow))))
$Sheet1.QueryTables.item($mcConnector.name).TextFileCommaDelimiter = $True
$Sheet1.QueryTables.item($mcConnector.name).TextFileParseType = 1
[void]$Sheet1.QueryTables.item($mcConnector.name).Refresh()
if ($mcCurrentRow -ne 1) { [void]($Sheet1.Cells.Item($mcCurrentRow,1).entireRow).delete()} # Delete header on 2nd and later CSV.
$mcCurrentRow = $Sheet1.UsedRange.EntireRow.Count+1
} else {
Write-host ' *** Max [' $mcMaxImport '] import events reached, skipping:' $mcfile
}
}
#----Customize XLS, Excel Globals--------------------------------------------------------
Write-Host 'Customizing XLS.' #https://msdn.microsoft.com/en-us/library/bb241425(v=office.12).aspx, https://msdn.microsoft.com/en-us/library/office/ff838592.aspx
$xlRowField = 1 #XlPivotFieldOrientation
$xlPageField = 3 #XlPivotFieldOrientation
$xlDataField = 4 #XlPivotFieldOrientation
$xlColumnField = 2 #XlPivotFieldOrientation
$xlAverage = -4106 #XlConsolidationFunction
$xlSum = -4157 #XlConsolidationFunction
$xlCount = -4112 #XlConsolidationFunction
$xlPercentOfTotal = 8 #XlPivotFieldCalculation
$xlPercentRunningTotal = 13 #XlPivotFieldCalculation
$mcNumberF = "###,###,###,###,###"
$mcPercentF = "#0.00%"
$mcDateF = "yyyy/m/d h:mm"
$mcDateGroupFlags=($false, $false, $true, $true, $true, $false, $false) # Months, Days, Hours, https://msdn.microsoft.com/en-us/library/office/ff839808.aspx
#-------#Sheet1 - RawData---------------------------------------------------------------
$Sheet1.Range("A1").Autofilter() | Out-Null
$Sheet1.Application.ActiveWindow.SplitRow = 1
$Sheet1.Application.ActiveWindow.FreezePanes = $true
Write-Host 'Tab 0.411 Events imported'
#-------#Sheet2 - PivotTable1---------------------------------------------------------------
$Sheet2 = $mcWorkbooks.Worksheets.add()
$PivotTable1 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$PivotTable1.CreatePivotTable("Sheet2!R1C1") | Out-Null
if ($MultiIP2 -eq $true) {
$mcPF00 = $Sheet2.PivotTables("PivotTable1").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
for ($i=1; $i -lt $mcPF00.PivotItems().Count; $i++){ $mcPF00.PivotItems($i).Visible=$false } #Deselect all but last one (blank) IP
} else {
$mcPF00 = $Sheet2.PivotTables("PivotTable1").PivotFields("Server")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
}
$mcPF0 = $Sheet2.PivotTables("PivotTable1").PivotFields("Account")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcPF1 = $Sheet2.PivotTables("PivotTable1").PivotFields("IPAddress1")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet2.PivotTables("PivotTable1").PivotFields("IPAddress1")
mcSetPivotField($mcPF, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet2, "PivotTable1", 60, 15, $null, $null, $null, $null, $null,"Account Name", "1.FEP.UPN.IP", $null, $null)
mcSortPivotFields($sheet2,$mcPF,$mcPF0)
mcSetPivotTableHeaderColor($Sheet2, "A")
Write-Host 'Tab 1.FEP.UPN.IP Done'
#-------#Sheet3 - PivotTable2---------------------------------------------------------------
$Sheet3 = $mcWorkbooks.Worksheets.add()
$PivotTable2 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$PivotTable2.CreatePivotTable("Sheet3!R1C1") | Out-Null
if ($MultiIP2 -eq $true) {
$mcPF00 = $Sheet3.PivotTables("PivotTable2").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
for ($i=1; $i -lt $mcPF00.PivotItems().Count; $i++){ $mcPF00.PivotItems($i).Visible=$false } #Deselect all but last one (blank) IP
} else {
$mcPF00 = $Sheet3.PivotTables("PivotTable2").PivotFields("Server")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
}
$mcPF0 = $Sheet3.PivotTables("PivotTable2").PivotFields("IPAddress1")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcPF1 = $Sheet3.PivotTables("PivotTable2").PivotFields("Account")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet3.PivotTables("PivotTable2").PivotFields("IPAddress1")
mcSetPivotField($mcPF, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet3, "PivotTable2", 40, 15, 40, 75, $null, $null, $null,"IP grouping", "2.FEP.IP.UPN", $null, $null)
mcSortPivotFields($Sheet3,$mcPF,$mcPF0)
mcSetPivotTableHeaderColor($Sheet3, "A")
$i=4 #Set GeoIP URL outside of Pivot Table.
while ($Sheet3.Cells.Item($i,1).text -Notcontains "Grand Total") {
$mcIP = $Sheet3.Cells.Item($i,1).text
[void]$Sheet3.Hyperlinks.add($Sheet3.Cells.Item($i,3),"http://whatismyipaddress.com/IP/"+$mcIP,"GeoInfo","Click to get GeoInfo","GeoInfo for: "+$mcIP)
$Sheet3.Cells.Item($i,4).Formula = $mcIP+','
$i++
}
[void]$Sheet3.Hyperlinks.add($Sheet3.Cells.Item(2,4),"https://technet.microsoft.com/en-us/library/jj554908.aspx","New-NetFirewallRule","Click to get Info on New-NetFirewallRule", "Sample firewall rule using New-NetFirewallRule and -Action Block -RemoteAddress on TechNet")
$Sheet3.Cells.Item(3,4).Formula='New-NetFirewallRule -Name "BlockedIpAddress" -Direction Inbound -Action Block -RemoteAddress "'
$Sheet3.Cells.Item($i,4).Formula='"'
Write-Host 'Tab 2.FEP.IP.UPN Done'
#-------#Sheet4 - PivotTable3---------------------------------------------------------------
$Sheet4 = $mcWorkbooks.Worksheets.add()
$PivotTable3 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$PivotTable3.CreatePivotTable("Sheet4!R1C1") | Out-Null
if ($MultiIP2 -eq $true) {
$mcPF00 = $Sheet4.PivotTables("PivotTable3").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
for ($i=1; $i -lt $mcPF00.PivotItems().Count; $i++){ $mcPF00.PivotItems($i).Visible=$false } #Deselect all but last one (blank) IP
} else {
$mcPF00 = $Sheet4.PivotTables("PivotTable3").PivotFields("Server")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
}
$mcPF0 = $Sheet4.PivotTables("PivotTable3").PivotFields("DateTime")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcCells=$mcPF0.DataRange.Item(3)
if ($SingleIP1 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPF1 = $Sheet4.PivotTables("PivotTable3").PivotFields("IPAddress1")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet4.PivotTables("PivotTable3").PivotFields("Account")
mcSetPivotField($mcPF, $xlRowField, $null, $null, $null, $null, $null)
$mcPFc = $Sheet4.PivotTables("PivotTable3").PivotFields("DateTime")
mcSetPivotField($mcPFc, $xlColumnField, $mcNumberF, $null, $null, $null, "Hours",1)
if ($SingleIP1 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPFd = $Sheet4.PivotTables("PivotTable3").PivotFields("IPAddress1")
mcSetPivotField($mcPFd, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet4, "PivotTable3", 60, 15, $null, $null, $null, $null, $null,"Time Grouping", "3.FEP.IP.UPN.Time", $null, $null)
mcSetPivotTableHeaderColor($Sheet4, "A")
$PivotTable3.Application.ActiveWindow.SplitRow = 6
$PivotTable3.Application.ActiveWindow.FreezePanes = $true
$mcPF1.ShowDetail = $false
$i=5 #Sort on GrandTotal > variable column due to time range
$mcGrandTotalCol=$Sheet4.Range("A4").CurrentRegion.Columns.Count
while ($Sheet4.cells.Item($i,$mcGrandTotalCol).formula -le 0) { $i++ } #find first none blank below GrandTotal
[void]$Sheet4.cells.Item($i,$mcGrandTotalCol).sort($Sheet4.cells.Item($i,$mcGrandTotalCol),2)
$Sheet4.Range("A"+$i).ShowDetail = $true #collapse on Account except first item.
[void]$Sheet4.cells.Item($i+1,$mcGrandTotalCol).sort($Sheet4.cells.Item($i+1,$mcGrandTotalCol),2)
Write-Host 'Tab 3.FEP.IP.UPN.Time Done'
#-------#Sheet5 - pivotTable4---------------------------------------------------------------
$Sheet5 = $mcWorkbooks.Worksheets.add()
$pivotTable4 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$pivotTable4.CreatePivotTable("Sheet5!R1C1") | Out-Null
if ($MultiIP2 -eq $true) {
$mcPF00 = $Sheet5.PivotTables("pivotTable4").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
for ($i=1; $i -lt $mcPF00.PivotItems().Count; $i++){ $mcPF00.PivotItems($i).Visible=$false } #Deselect all but last one (blank) IP
} else {
$mcPF00 = $Sheet5.PivotTables("pivotTable4").PivotFields("Server")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
}
$mcPF0 = $Sheet5.PivotTables("pivotTable4").PivotFields("DateTime")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcCells=$mcPF0.DataRange.Item(3)
if ($SingleIP1 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPF1 = $Sheet5.PivotTables("pivotTable4").PivotFields("Account")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet5.PivotTables("pivotTable4").PivotFields("IPAddress1")
mcSetPivotField($mcPF, $xlRowField, $null, $null, $null, $null, $null)
$mcPFc = $Sheet5.PivotTables("pivotTable4").PivotFields("DateTime")
mcSetPivotField($mcPFc, $xlColumnField, $mcNumberF, $null, $null, $null, "Hours",1)
if ($SingleIP1 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPFd = $Sheet5.PivotTables("pivotTable4").PivotFields("IPAddress1")
mcSetPivotField($mcPFd, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet5, "pivotTable4", 60, 15, $null, $null, $null, $null, $null,"Time Grouping", "4.FEP.UPN.IP.Time", $null, $null)
mcSetPivotTableHeaderColor($Sheet5, "A")
$pivotTable4.Application.ActiveWindow.SplitRow = 6
$pivotTable4.Application.ActiveWindow.FreezePanes = $true
$mcPF1.ShowDetail = $false
$i=5 #Sort on GrandTotal > variable column due to time range
$mcGrandTotalCol=$Sheet5.Range("A4").CurrentRegion.Columns.Count
while ($Sheet5.cells.Item($i,$mcGrandTotalCol).formula -le 0) { $i++ } #find first none blank below GrandTotal
[void]$Sheet5.cells.Item($i,$mcGrandTotalCol).sort($Sheet5.cells.Item($i,$mcGrandTotalCol),2)
$Sheet5.Range("A"+$i).ShowDetail = $true #collapse on Account except first item.
[void]$Sheet5.cells.Item($i+1,$mcGrandTotalCol).sort($Sheet5.cells.Item($i+1,$mcGrandTotalCol),2)
Write-Host 'Tab 4.FEP.UPN.IP.Time Done'
#----------------------------------------------------------------------
If ($MultiIP2 -eq $true) { # PivotTables for External IP addresses
#-------#Sheet6 - pivotTable5---------------------------------------------------------------
$Sheet6 = $mcWorkbooks.Worksheets.add()
$pivotTable5 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$pivotTable5.CreatePivotTable("Sheet6!R1C1") | Out-Null
$mcPF00 = $Sheet6.PivotTables("pivotTable5").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
$mcPF00.PivotItems($mcPF00.PivotItems().Count).Visible=$false #Deselect last (blank) IP
$mcPF0 = $Sheet6.PivotTables("pivotTable5").PivotFields("Account")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcPF1 = $Sheet6.PivotTables("pivotTable5").PivotFields("IPAddress1")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet6.PivotTables("pivotTable5").PivotFields("IPAddress1")
mcSetPivotField($mcPF, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet6, "pivotTable5", 60, 15, $null, $null, $null, $null, $null,"Account Name", "5.EXO.UPN.IP", $null, $null)
mcSortPivotFields($Sheet6,$mcPF,$mcPF0)
mcSetPivotTableHeaderColor($Sheet6, "A")
Write-Host 'Tab 5.EXO.UPN.IP Done'
#-------#Sheet7 - PivotTable6---------------------------------------------------------------
$Sheet7 = $mcWorkbooks.Worksheets.add()
$PivotTable6 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$PivotTable6.CreatePivotTable("Sheet7!R1C1") | Out-Null
$mcPF00 = $Sheet7.PivotTables("PivotTable6").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
$mcPF00.PivotItems($mcPF00.PivotItems().Count).Visible=$false #Deselect last (blank) IP
$mcPF0 = $Sheet7.PivotTables("PivotTable6").PivotFields("IPAddress1")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcPF1 = $Sheet7.PivotTables("PivotTable6").PivotFields("Account")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet7.PivotTables("PivotTable6").PivotFields("IPAddress1")
mcSetPivotField($mcPF, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet7, "PivotTable6", 40, 15, 40, 75, $null, $null, $null,"IP grouping", "6.EXO.IP.UPN", $null, $null)
mcSortPivotFields($Sheet7,$mcPF,$mcPF0)
mcSetPivotTableHeaderColor($Sheet7, "A")
$i=4 #Set GeoIP URL outside of Pivot Table.
while ($Sheet7.Cells.Item($i,1).text -Notcontains "Grand Total") {
$mcIP = $Sheet7.Cells.Item($i,1).text
[void]$Sheet7.Hyperlinks.add($Sheet7.Cells.Item($i,3),"http://whatismyipaddress.com/IP/"+$mcIP,"GeoInfo","Click to get GeoInfo","GeoInfo for: "+$mcIP)
$Sheet7.Cells.Item($i,4).Formula = '"'+$mcIP+'",'
$i++
}
[void]$Sheet7.Hyperlinks.add($Sheet7.Cells.Item(2,4),"https://technet.microsoft.com/en-us/library/aa997443(v=exchg.160).aspx","Set-OrganizationConfig","Click to get Info on Set-OrganizationConfig", "Info for Set-OrganizationConfig and -IPListBlocked on TechNet")
$Sheet7.Cells.Item(3,4).Formula='Set-OrganizationConfig -IPListBlocked@{add='
$Sheet7.Cells.Item($i,4).Formula='}'
$Sheet7.Cells.Item(3,5).Formula='Note: Be sure to verify your block list "get-OrganizationConfig | select -ExpandProperty IPListBlocked"'
Write-Host 'Tab 6.EXO.IP.UPN Done'
#-------#Sheet8 - pivotTable7---------------------------------------------------------------
$Sheet8 = $mcWorkbooks.Worksheets.add()
$pivotTable7 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$pivotTable7.CreatePivotTable("Sheet8!R1C1") | Out-Null
$mcPF00 = $Sheet8.PivotTables("pivotTable7").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
$mcPF00.PivotItems($mcPF00.PivotItems().Count).Visible=$false #Deselect last (blank) IP
$mcPF0 = $Sheet8.PivotTables("pivotTable7").PivotFields("DateTime")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcCells=$mcPF0.DataRange.Item(3)
if ($SingleIP2 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPF1 = $Sheet8.PivotTables("pivotTable7").PivotFields("IPAddress1")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet8.PivotTables("pivotTable7").PivotFields("Account")
mcSetPivotField($mcPF, $xlRowField, $null, $null, $null, $null, $null)
$mcPFc = $Sheet8.PivotTables("pivotTable7").PivotFields("DateTime")
mcSetPivotField($mcPFc, $xlColumnField, $mcDateF, $null, $null, $null, "Hours",1)
if ($SingleIP2 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPFd = $Sheet8.PivotTables("pivotTable7").PivotFields("IPAddress1")
mcSetPivotField($mcPFd, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet8, "pivotTable7", 60, 15, $null, $null, $null, $null, $null,"Time Grouping", "7.EXO.IP.UPN.Time", $null, $null)
mcSetPivotTableHeaderColor($Sheet8, "A")
$pivotTable7.Application.ActiveWindow.SplitRow = 6
$pivotTable7.Application.ActiveWindow.FreezePanes = $true
$mcPF1.ShowDetail = $false
$i=5 #Sort on GrandTotal > variable column due to time range
$mcGrandTotalCol=$Sheet8.Range("A4").CurrentRegion.Columns.Count
while ($Sheet8.cells.Item($i,$mcGrandTotalCol).formula -le 0) { $i++ } #find first none blank below GrandTotal
[void]$Sheet8.cells.Item($i,$mcGrandTotalCol).sort($Sheet8.cells.Item($i,$mcGrandTotalCol),2)
$Sheet8.Range("A"+$i).ShowDetail = $true #collapse on Account except first item.
[void]$Sheet8.cells.Item($i+1,$mcGrandTotalCol).sort($Sheet8.cells.Item($i+1,$mcGrandTotalCol),2)
Write-Host 'Tab 7.EXO.IP.UPN.Time Done'
#-------#Sheet9 - pivotTable8---------------------------------------------------------------
$Sheet9 = $mcWorkbooks.Worksheets.add()
$pivotTable8 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$pivotTable8.CreatePivotTable("Sheet9!R1C1") | Out-Null
$mcPF00 = $Sheet9.PivotTables("pivotTable8").PivotFields("IPAddress2")
mcSetPivotField($mcPF00, $xlPageField, $null, $null, $null, $null, $null)
$mcPF00.PivotItems($mcPF00.PivotItems().Count).Visible=$false #Deselect last (blank) IP
$mcPF0 = $Sheet9.PivotTables("pivotTable8").PivotFields("DateTime")
mcSetPivotField($mcPF0, $xlRowField, $null, $null, $null, $null, $null)
$mcCells=$mcPF0.DataRange.Item(3)
if ($SingleIP2 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPF1 = $Sheet9.PivotTables("pivotTable8").PivotFields("Account")
mcSetPivotField($mcPF1, $xlRowField, $null, $null, $null, $null, $null)
$mcPF = $Sheet9.PivotTables("pivotTable8").PivotFields("IPAddress1")
mcSetPivotField($mcPF, $xlRowField, $null, $null, $null, $null, $null)
$mcPFc = $Sheet9.PivotTables("pivotTable8").PivotFields("DateTime")
mcSetPivotField($mcPFc, $xlColumnField, $mcDateF, $null, $null, $null, "Hours",1)
if ($SingleIP2 -ne $true) { $mcCells.group($true,$true,1,$mcDateGroupFlags) | Out-Null }
$mcPFd = $Sheet9.PivotTables("pivotTable8").PivotFields("IPAddress1")
mcSetPivotField($mcPFd, $xlDataField, $mcNumberF, $null, $null, $null, "Address Count",1)
mcSetPivotTableFormat($Sheet9, "pivotTable8", 60, 15, $null, $null, $null, $null, $null,"Time Grouping", "8.EXO.UPN.IP.Time", $null, $null)
mcSetPivotTableHeaderColor($Sheet9, "A")
$pivotTable8.Application.ActiveWindow.SplitRow = 6
$pivotTable8.Application.ActiveWindow.FreezePanes = $true
$mcPF1.ShowDetail = $false
$i=5 #Sort on GrandTotal > variable column due to time range
$mcGrandTotalCol=$Sheet9.Range("A4").CurrentRegion.Columns.Count
while ($Sheet9.cells.Item($i,$mcGrandTotalCol).formula -le 0) { $i++ } #find first none blank below GrandTotal
[void]$Sheet9.cells.Item($i,$mcGrandTotalCol).sort($Sheet9.cells.Item($i,$mcGrandTotalCol),2) #Sort on Name
$Sheet9.Range("A"+$i).ShowDetail = $true #collapse on Account except first item.
[void]$Sheet9.cells.Item($i+1,$mcGrandTotalCol).sort($Sheet9.cells.Item($i+1,$mcGrandTotalCol),2) #Sort on IP
Write-Host 'Tab 8.EXO.UPN.IP.Time Done'
#-------#$Sheet10 - $PivotTable9---------------------------------------------------------------
$Sheet10 = $mcWorkbooks.Worksheets.add()
$PivotTable9 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$PivotTable9.CreatePivotTable("Sheet10!R1C1") | Out-Null
$Sheet10.name = "9.CustomPivot"
#----------------------------------------------------------------------
$Sheet6.Tab.ColorIndex = $Sheet7.Tab.ColorIndex = $Sheet8.Tab.ColorIndex = $Sheet9.Tab.ColorIndex = 36
} else { #Single IP address dataset
#-------#$Sheet6 - $PivotTable5---------------------------------------------------------------
$Sheet6 = $mcWorkbooks.Worksheets.add()
$PivotTable5 = $mcWorkbooks.PivotCaches().Create(1,"Sheet1!R1C1:R$($Sheet1.UsedRange.Rows.count)C$($Sheet1.UsedRange.Columns.count)",5) # xlDatabase=1 xlPivotTableVersion15=5 Excel2013
$PivotTable5.CreatePivotTable("Sheet6!R1C1") | Out-Null
$Sheet6.name = "5.CustomPivot"
}
#Set Sort sheet names in reverse
$Sheet1.Name = "0.411.Events"
$Sheet2.Tab.ColorIndex = $Sheet3.Tab.ColorIndex = $Sheet4.Tab.ColorIndex = $Sheet5.Tab.ColorIndex = 35
$mcWorkSheetNames = New-Object System.Collections.ArrayList
foreach ($mcWorkSheet in $mcWorkbooks.Worksheets) { $mcWorkSheetNames.add($mcWorkSheet.Name) | Out-null }
$mctmp = $mcWorkSheetNames.Sort() | Out-Null
For ($i=0; $i -lt $mcWorkSheetNames.Count-1; $i++){ #Sort name.
$mcTmp = $mcWorkSheetNames[$i]
$mcBefore = $mcWorkbooks.Worksheets.Item($mcTmp)
$mcAfter = $mcWorkbooks.Worksheets.Item($i+1)
$mcBefore.Move($mcAfter)
}
$Sheet1.Activate()
if ($mcScriptPrompts) { #SaveAsFile
$mcFileName = Read-Host "Enter a FileName to save extracted event 411 xlsx.`n"
if ($mcFileName) {
Write-Host "Saving file to $mcEvtPath\$mcFileName.xlsx"
$mcWorkbooks.SaveAs($mcEvtPath+'\'+$mcFileName)
}
$mcCleanup = Read-Host "Delete 411-*.CSV? ([Enter]/[Y] to delete, [N] to keep csv)`n"
if ($mcCleanup -ne 'N') {
Get-ChildItem -Path $mcEvtPath | Where {$_.name -clike '411-*.csv'} | foreach ($_) {
Remove-Item $mcEvtPath'\'$_
write-host ' '$_ deleted.
}
}
}
$mcExcel.visible = $true
# mcCleanUpExcelObj
} else { #end of mcFiles
write-host ' No 411-*.csv found in specified directory.' $mcEvtPath
}
}
Write-Host 'Script completed.'
Block Connection to Exchange Online
In this case, the bad actor is taking advantage of Exchange Online basic authentication (also known as legacy authentication) so that the client IP address appears as a Microsoft one.
The application side of basic authentication is for use in older mail protocols like IMAP, POP, and SMTP.
1. Blocking IP's in Exchange Online (EXO):
Add suspicious IP's to a block list in the EXO configuration. Using the syntax below, you can add more IP's to that list as needed.
Note, once IP’s are added to the block list it will take 4hrs to fully propagate and take effect.
Set-OrganizationConfig -IPListBlocked
- If you haven’t already done so on the PS client being used, run the following: Set-ExecutionPolicy RemoteSigned
- Connect to EXO using steps 1-3 in the link below with Global Admin or Exchange admin security context: https://technet.microsoft.com/en-us/library/jj984289(v=exchg.160).aspx
- If you have enabled MFA for your Administrator, please follow the steps in the document to get connect to EXO PowerShell: https://technet.microsoft.com/en-us/library/mt775114(v=exchg.160).aspx
- Enable the block list for the IP address assigned to the test device used in step 1
Set-OrganizationConfig <tenant name="">.onmicrosoft.com -IPListBlocked 127.0.0.2
Set-OrganizationConfig <tenant name="">.onmicrosoft.com -IPListBlocked@{add="198.76.9.23", "172.16.0.0-172.31.255.255","2001:db8:0:1234:0:567:8:1", "2001:0DB8::CD3/60","192.168.8.2(255.255.255.0)","2001:db8::1","2001:0DB8:0000:CD30:0000:0000:0000:0000/60","ABCD:EF01:2345:6789:ABCD:EF01:2345:6789"}
Set-OrganizationConfig -Identity <tenant name="">
-IPListBlocked @{remove="0.0.0.0"}
Verify your block list submission
Get-OrganizationConfig <tenant name="">.onmicrosoft.com | select -ExpandProperty IPListBlocked
2001:0DB8:0000:CD30:0000:0000:0000:0000/60
2001:db8::1192.168.8.2(255.255.255.0)
2001:0DB8::CD3/60
2001:db8:0:1234:0:567:8:1
172.16.0.0-172.31.255.255
198.76.9.23
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
127.0.0.2
Disable Legacy Authentication in Exchange Online
Set-OrganizationConfig -OAuth2ClientProfileEnabled $true
New-AuthenticationPolicy -Name "Block Basic Auth"
2. Assign the Authentication Policy to users
Set-User -Identity laura@contoso.com -AuthenticationPolicy "Block Basic Auth"
3. Configure the defualt authentication policy. The default authentication policy is assigned to all users who don't already have a specific policy assigned to them. Note that the authentication policies assigned to users take precedence to the default policy.
Set-OrganizationConfig -DefaultAuthenticationPolicy "Block Basic Auth"
- App passwords: For more information about app passwords, see Create an app password for Office 365.
- Availability address spaces: These contain a service account that's used to share calendar free/busy information in hybrid and federated deployments. The service account authenticates with a username and password, so blocking Basic authentication blocks the authentication flow. For more information about availability address spaces in hybrid deployments, see Demystifying Hybrid Free/Busy
Identify Targeted User IPs
- You can download the PowerShell script to search your AD FS servers for events 411 at this link. The script will provide a CSV file which contains the UserPrincipalName, IP address of submitter, and time of all bad credential submissions to your AD FS farm.
- You can open the CSV in Excel and quickly filter by username, or IP or times.
PARAM ($PastDays = 1, $PastHours)
#************************************************
# ADFSBadCredsSearch.ps1
# Version 1.0
# Date: 6-20-2016
# Author: Tim Springston [MSFT]
# Description: This script will parse the AD FS server's (not proxy) security AD FS
# for events which indicate an incorrectly entered username or password. The script can specify a
# past period to search the log for and it defaults to the past 24 hours. Results will be placed into a CSV for
# review of UPN, IP address of submitter, and timestamp.
#************************************************
cls
if ($PastHours -gt 0)
{$PastPeriod = (Get-Date).AddHours(-($PastHours))}
else
{$PastPeriod = (Get-Date).AddDays(-($PastDays)) }
$Outputfile = $Pwd.path + "\BadCredAttempts.csv"
$CS = get-wmiobject -class win32_computersystem
$Hostname = $CS.Name + '.' + $CS.Domain
$Instances = @{}
$OSVersion = gwmi win32_operatingsystem
[int]$BN = $OSVersion.Buildnumber
if ($BN -lt 9200){$ADFSLogName = "AD FS 2.0/Admin"}
else {$ADFSLogName = "AD FS/Admin"}
$Users = @()
$IPAddresses = @()
$Times = @()
$AllInstances = @()
Write-Host "Searching event log for bad credential events..."
if ($BN -ge 9200) {Get-Winevent -FilterHashTable @{LogName= "Security"; StartTime=$PastPeriod; ID=411} -ErrorAction SilentlyContinue | Where-Object {$_.Message -match "The user name or password is incorrect"} | % {
$Instance = New-Object PSObject
$UPN = $_.Properties[2].Value
$UPN = $UPN.Split("-")[0]
$IPAddress = $_.Properties[4].Value
$Users += $UPN
$IPAddresses += $IPAddress
$Times += $_.TimeCreated
add-member -inputobject $Instance -membertype noteproperty -name "UserPrincipalName" -value $UPN
add-member -inputobject $Instance -membertype noteproperty -name "IP Address" -value $IPAddress
add-member -inputobject $Instance -membertype noteproperty -name "Time" -value ($_.TimeCreated).ToString()
$AllInstances += $Instance
$Instance = $null
}
}
$AllInstances | select * | Export-Csv -Path $Outputfile -append -force -NoTypeInformation
Write-Host "Data collection finished. The output file can be found at $outputfile`."
$AllInstances = $null
- These events will contain the user principal name (UPN) of the targeted user.
- These events will also contain a message "token validation failed" and will say if it was a bad password attempt or the account is locked out.
Check if Lockout is Enabled
Make sure you have all Windows Server 2016 servers up to date as of the March 2018 Windows Updates.
Update Artifact Db Permissions
Extranet smart lockout requires the AD FS service account to have permissions to create a new table in the AD FS artifact database.
Log in to any AD FS server as an AD FS admin, and then grant this permission by executing the following commands in a PowerShell Command Prompt window:
$cred = Get-Credential
Update-AdfsArtifactDatabasePermission -Credential $cred
Note: The $cred placeholder is an account that has AD FS administrator permissions. This should provide the write permissions to create the table.
ALTER AUTHORIZATION ON SCHEMA::[ArtifactStore] TO [db_genevaservice]
Configure Extranet Smart Lockout
A new parameter that is named ExtranetLockoutMode is added to support ESL. It contains the following values:
- ADPasswordCounter –This is the legacy AD FS "extranet soft lockout" mode, which does not differentiate based on location. This is the default value.
- ADFSSmartLockoutLogOnly –This is Extranet Smart Lockout. Instead of rejecting authentication requests, AD FS writes admin and audit events.
- ADFSSmartLockoutEnforce –This is Extranet Smart Lockout with full support for blocking unfamiliar requests when thresholds are reached.
Set-AdfsProperties -ExtranetLockoutMode AdfsSmartlockoutLogOnly
In this mode, AD FS performs the analysis but does not block any requests because of lockout counters.
This mode is used to validate that smart lockout is running successfully before it enables "enforce" mode.
Restart-service adfssrv
Set Account Lockout Threshold
Every time that a password-based authentication is successful, AD FS stores the client IPs as familiar locations in the account activity table.
Failed password attemps are tracked with a familiar location counter and a separate new unfamiliar location counter. After a number of failed password attempts reaches the lockout threshold, the account is locked out.
The threshold is set by using Set-AdfsProperties.
Set-AdfsProperties -ExtranetLockoutThreshold 10
Set Observation Window
The observation window setting allows an account to automatically unlock after some time. After the account unlocks, one authentication attempt is allowed. If the authentication succeeds, the failed authentication count is reset to 0. If it fails, the system waits for another observation window before the user can try again.
The observation window is set by using Set-AdfsProperties as in the following example command:
Set-AdfsProperties -ExtranetObservationWindow ( new-timespan -minutes 5 )
Enable Lockout
Extranet lockout can be enabled or disabled by using the EnableExtranetLockout parameter as in the following examples.
To enable lockout, run the following command:
Set-AdfsProperties -EnableExtranetLockout $true
Set-AdfsProperties -EnableExtranetLockout $false
Enforce Lockout
After you're comfortable with the lockout threshold and observation window, ESL can be moved to "enforce" mode by using the following PSH cmdlet:
Set-AdfsProperties -ExtranetLockoutMode AdfsSmartLockoutEnforce
For the new mode to take effect, restart the AD FS service on all nodes in the farm by using the following command:
Restart-service adfssrv
Add IPs to the AD FS Banned IP List.
Set-AdfsProperties -AddBannedIps "1.2.3.4", "::3", "1.2.3.4/16"
Allowed formats:
- IPv6
- CIDR format with IPv4 or v6
- IP range with IPv4 or v6 ( i.e. 1.2.3.4-1.2.3.6 )
Steps Completed
Additional recommendations are:
- Upgrading to the latest version of AD FS.
- Blocking legacy authentication from the extranet.
- Deploying Azure AD Connect Health agents for AD FS on all your AD FS servers.
- Consider using a passwordless primary authentication method such as Azure MFA, certificates, or Windows Hello for Business.
Let us know how to improve our guide.
Our general recommendations around Extranet Lockout are are:
- Upgrading to the latest version of AD FS.
- Blocking legacy authentication from the extranet.
- Deploying Azure AD Connect Health agents for AD FS on all your AD FS servers.
- Consider using a passwordless primary authentication method such as Azure MFA, certificates, or Windows Hello for Business.
Configure your AD.
- Navigate to Start – Administrative Tools – Group Policy Management.
- Expand the relevant domain node. Right click Default Domain Policy and select Edit from the drop down list.
- Group Policy Management Editor opens. Navigate to Computer Configuration\Policies \Windows Settings \Security Settings \Account Policies \Account Lockout Policy where three lockout policy settings listed.
- To set the Account Lockout Threshold policy setting, right click it and select Properties from the drop down list.
- The Account Lockout Threshold properties dialog box opens. For our example, we amend the lockout threshold number to 12. Click OK to apply the changes.
- You are informed that since the Account Lockout Threshold policy setting has been given a value, Windows Server automatically defines and applies a security setting of 30 minutes to the other policy settings (Account Lockout Duration and Reset Account Lockout Counter After). Click OK to continue.
- The Account Lockout Threshold has now been successfully configured. The other policy settings, Account Lockout Duration and Reset Account Lockout Counter After, also have been updated.
- If you prefer that a user account is locked out until an administrator unlocks it again, open the Account Lockout Duration properties dialog box. Enter in ‘0’ to the text box and click OK.
Configure Extranet Lockout on AD FS 2012R2.
There are 3 settings you have to enable to configure ESL:
- EnableExtranetLockout: set this Boolean value to be True if you want to enable Extranet Lockout.
- ExtranetLockoutThreshold: this defines the maximum number of bad password attempts. Once the threshold is reached, AD FS will immediately rejects the requests from extranet without attempting to contact the domain controller for authentication, no matter whether password is good or bad, until the extranet observation window is passed. This means the value of badPwdCount attribute of an AD account will not increase while the account is soft-locked out.
- ExtranetObservationWindow this determines for how long the user account will be soft-locked out. AD FS will start to perform username and password authentication again when the window is passed. AD FS uses the AD attribute badPasswordTime as the reference for determining whether the extranet observation window has passed or not. The window has passed if current time > badPasswordTime + ExtranetObservationWindow.
Set your values using PowerShell.
This example below allows you to set the Extranet Lockout feature with maximum of 15 number of bad password attempts and 30 mins soft-lockout duration. Change the values to fit your scenario.
Set-AdfsProperties -EnableExtranetLockout $true -ExtranetLockoutThreshold 15 -ExtranetObservationWindow (new-timespan -Minutes 30)
Configure Extranet Smart Lockout
A new parameter that is named ExtranetLockoutMode is added to support ESL. It contains the following values:
- ADPasswordCounter –This is the legacy AD FS "extranet soft lockout" mode, which does not differentiate based on location. This is the default value.
- ADFSSmartLockoutLogOnly –This is Extranet Smart Lockout. Instead of rejecting authentication requests, AD FS writes admin and audit events.
- ADFSSmartLockoutEnforce –This is Extranet Smart Lockout with full support for blocking unfamiliar requests when thresholds are reached.
Set-AdfsProperties -ExtranetLockoutMode AdfsSmartlockoutLogOnly
In this mode, AD FS performs the analysis but does not block any requests because of lockout counters.
This mode is used to validate that smart lockout is running successfully before it enables "enforce" mode.
Restart-service adfssrv
Set Account Lockout Threshold
Every time that a password-based authentication is successful, AD FS stores the client IPs as familiar locations in the account activity table.
If password-based authentication fails and the credentials do not come from a familiar location, the failed authentication count is incremented.
After the number of failed password attempts from unfamiliar locations reaches the lockout threshold, if password-based authentication from an unfamiliar location fails, the account is locked out.
Note Lockout continues to apply to familiar locations separately from this new unfamiliar lockout counter.
The threshold is set by using Set-AdfsProperties.
Set-AdfsProperties -ExtranetLockoutThreshold 10
Set Observation Window
The observation window setting allows an account to automatically unlock after some time. After the account unlocks, one authentication attempt is allowed. If the authentication succeeds, the failed authentication count is reset to 0. If it fails, the system waits for another observation window before the user can try again.
The observation window is set by using Set-AdfsProperties as in the following example command:
Set-AdfsProperties -ExtranetObservationWindow ( new-timespan -minutes 5 )
Enable Lockout
Extranet lockout can be enabled or disabled by using the EnableExtranetLockout parameter as in the following examples.
To enable lockout, run the following command:
Set-AdfsProperties -EnableExtranetLockout $true
Set-AdfsProperties -EnableExtranetLockout $false
Enforce Lockout
After you're comfortable with the lockout threshold and observation window, ESL can be moved to "enforce" mode by using the following PSH cmdlet:
Set-AdfsProperties -ExtranetLockoutMode AdfsSmartLockoutEnforce
For the new mode to take effect, restart the AD FS service on all nodes in the farm by using the following command:
Restart-service adfssrv
Add External IPs to Banned IP List.
Set-AdfsProperties -AddBannedIps "1.2.3.4", "::3", "1.2.3.4/16"
Allowed formats:
- IPv6
- CIDR format with IPv4 or v6
- Navigate to the AD FS directory, at %WINDIR%\adfs\config.
- IP range with IPv4 or v6 ( i.e. 1.2.3.4-1.2.3.6 )