Hunting for Basic Authentication in AzureAD

Hallo zusammen,

Im Exchange Team Blog wurde bereits vor fast einem Jahr die Abschaltung der Basic Authentication für die zweite Jahreshälfte 2021 angekündigt. Es wird also Zeit, die Applikationen und Clients mit Basic Authentication zu jagen und auf neuere Authentication Methoden umzustellen.

Basic Authentication and Exchange Online – April 2020 Update
https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-and-exchange-online-april-2020-update/ba-p/1275508

Gerade als ich den Artikel geschrieben habe, gab es ein Update:

Basic Authentication and Exchange Online – February 2021 Update
https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-and-exchange-online-february-2021-update/ba-p/2111904

In Azure Active Directory unter Sign-Ins kann man mit "Add Filter" nach "Client App" filtern.

Nun werden die ClientApps mit Legacy Authentication (sprich: Basic Auth) angezeigt.

Im letzten Monat hat sich zweimal der ewservice@icewolf.ch versucht mit Basic Auth anzumelden. Einmal erfolgreich und einmal mit falschem Passwort.

Ich speichere die Azure AD Logs in einem LogAnalytics Workspace

Seit ein paar Monaten gibt es hier neue Logs, wie die NonInteractiveSignInLogins, ServicePrincipalSignInLogs, ManagedIdentitySingnInLogs und die ProvisioningLogs.

Unter den Logs sieht man genau diese Tabellen.

Nun durchforste ich mit einem KQL Query die SignInLogs. Wie man sieht, gibt es für den user ewservice@icewolf.ch zwei Logins. ResultType = 0 heisst, die Anmeldung war erfolgreich.

SigninLogs | where UserPrincipalName == ewservice@icewolf.ch

Mit einem Advanced Query, wird die Abfrage um eine virtuelle Spalte "isLegacyAuth" erweitert und in der where Klausel suche ich nach BasicAuth und erfolgreichen Anmeldungen. Ich summiere das ganze für den ausgewählten Zeitraum nach UserprincipalName und AppDisplayName.

let data = SigninLogs
| extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
| where isLegacyAuth == "Yes" and ResultType==0;
data
| summarize count() by UserPrincipalName, AppDisplayName

//Legacy Auth
SigninLogs
| where ResultType==0
| where ClientAppUsed == "Autodiscover" or ClientAppUsed == "Exchange ActiveSync" or ClientAppUsed == "Exchange Online Powershell" or ClientAppUsed == "Exchange Web Services" or ClientAppUsed == "IMAP" or ClientAppUsed == "POP3" or ClientAppUsed == "MAPI over HTTP" or ClientAppUsed == "Offline Address Book" or ClientAppUsed == "Other Clients" or ClientAppUsed == "Outlook Anywhere (RPC over HTTPS)" or ClientAppUsed == "POP" or ClientAppUsed == "Reporting Web Services" or ClientAppUsed == "SMTP" or ClientAppUsed == "Universal Outlook"
//| summarize count() by UserPrincipalName, AppDisplayName
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc

//Legacy Auth ohne ActiveSync
SigninLogs
| where ResultType==0
| where ClientAppUsed == "Autodiscover" or ClientAppUsed == "Exchange Online Powershell" or ClientAppUsed == "Exchange Web Services" or ClientAppUsed == "IMAP" or ClientAppUsed == "POP3" or ClientAppUsed == "MAPI over HTTP" or ClientAppUsed == "Offline Address Book" or ClientAppUsed == "Other Clients" or ClientAppUsed == "Outlook Anywhere (RPC over HTTPS)" or ClientAppUsed == "POP" or ClientAppUsed == "Reporting Web Services" or ClientAppUsed == "SMTP" or ClientAppUsed == "Universal Outlook"
//| summarize count() by UserPrincipalName, AppDisplayName
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc

//Sucessful POP3 / IMAP Logins
SigninLogs
| where TimeGenerated > ago(30d)
| where ClientAppUsed == "IMAP4" or ClientAppUsed == "POP3"
| where ResultType == 0
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc

//Unsucessful POP3 / IMAP Logins
SigninLogs
| where TimeGenerated > ago(30d)
| where ClientAppUsed == "IMAP4" or ClientAppUsed == "POP3"
| where ResultType != 0
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc

// Sucessful EWS Logins
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType==0
| where ClientAppUsed == "Exchange Web Services"
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc

// Sucessful SMTP Auth
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType==0
| where ClientAppUsed == "Authenticated SMTP"
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc

// Sucessful Logins (without POP3/IMAP/EWS/SMTPAuth)
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType==0
| where ClientAppUsed == "Autodiscover" or ClientAppUsed == "Exchange Online Powershell" or ClientAppUsed == "MAPI over HTTP" or ClientAppUsed == "Offline Address Book" or ClientAppUsed == "Other Clients" or ClientAppUsed == "Outlook Anywhere (RPC over HTTPS)" or ClientAppUsed == "Reporting Web Services" or ClientAppUsed == "Universal Outlook" or ClientAppUsed == "Exchange ActiveSync"
| summarize count() by UserPrincipalName, ClientAppUsed
| order by count_ desc


Da ich schon dabei bin, habe ich mir auch die anderen Logs angeschaut.

AADNonInteractiveUserSignInLogs

Oder in der Tabelle AADServicePrincipalSignInLogs nach einer mir bekannten AppID gesucht.

AADServicePrincipalSignInLogs | where AppId == "9a8d72df-686a-496c-bc5e-a147d813abd1"

Wer es etwas einfacher mag, schaut sich die Workbooks an. Dort gibt es auch einen "Sign-ins using Legacy Authentication" Report
Teile meines Querys habe ich übrigens von hier "geklaut". Einfach auf Edit und dann auf den Query Knopf klicken.
So erhält man Einsicht auf die dahinterliegende KQL Abfrage
let details = dynamic({"Name": "", "Type": "*"});
let data = SigninLogs
    | where AppDisplayName in ('*') or '*' in ('*')
    | where UserDisplayName in ('*') or '*' in ('*')
    | extend errorCode = toint(Status.errorCode)
    | extend SigninStatus = case(errorCode == 0, "Success",                      
        errorCode == 50058, "Interrupt",                      
        errorCode == 50140, "Interrupt",                      
        errorCode == 51006, "Interrupt",                      
        errorCode == 50059, "Interrupt",                      
        errorCode == 65001, "Interrupt",                      
        errorCode == 52004, "Interrupt",                      
        errorCode == 50055, "Interrupt",                      
        errorCode == 50144, "Interrupt",                      
        errorCode == 50072, "Interrupt",                      
        errorCode == 50074, "Interrupt",                      
        errorCode == 16000, "Interrupt",                      
        errorCode == 16001, "Interrupt",                      
        errorCode == 16003, "Interrupt",                      
        errorCode == 50127, "Interrupt",                      
        errorCode == 50125, "Interrupt",                      
        errorCode == 50129, "Interrupt",                      
        errorCode == 50143, "Interrupt",                      
        errorCode == 81010, "Interrupt",                      
        errorCode == 81014, "Interrupt",                      
        errorCode == 81012, "Interrupt",                      
        "Failure")
    | where SigninStatus == '*' or '*' == '*' or '*' == 'All Sign-ins'   
    | extend Reason = tostring(Status.failureReason)
    | extend ClientAppUsed = iff(isempty(ClientAppUsed) == true, "Unknown", ClientAppUsed) 
    | extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
    | where isLegacyAuth == "Yes"
    | where AppDisplayName in ('*') or '*' in ('*')
    | where details.Type == '*' or (details.Type == 'App' and AppDisplayName == details.Name) or (details.Type == 'Protocol' and AppDisplayName == details.ParentId and ClientAppUsed == details.Name);
data
| top 200 by TimeGenerated desc
| extend TimeFromNow = now() - TimeGenerated
| extend TimeAgo = strcat(case(TimeFromNow < 2m, strcat(toint(TimeFromNow / 1m), ' seconds'), TimeFromNow < 2h, strcat(toint(TimeFromNow / 1m), ' minutes'), TimeFromNow < 2d, strcat(toint(TimeFromNow / 1h), ' hours'), strcat(toint(TimeFromNow / 1d), ' days')), ' ago')
| project User = UserDisplayName, ['Sign-in Status'] = strcat(iff(SigninStatus == 'Success', '✔️', '❌'), ' ', SigninStatus), ['Sign-in Time'] = TimeAgo, App = AppDisplayName, ['Error code'] = errorCode, ['Result type'] = ResultType, ['Result signature'] = ResultSignature, ['Result description'] = ResultDescription, ['Conditional access policies'] = ConditionalAccessPolicies, ['Conditional access status'] = ConditionalAccessStatus, ['Operating system'] = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser, ['Country or region'] = LocationDetails.countryOrRegion, ['State'] = LocationDetails.state, ['City'] = LocationDetails.city, ['Time generated'] = TimeGenerated, Status, ['User principal name'] = UserPrincipalName

Und nun wünsche ich "Happy Hunting"
Grüsse
Andres Bohren