Table of Contents
Detecting MFA Fatigue
The following rule looks for instances where multiple MFA push notifications are sent to a given user and identifies scenarios where multiple failed push notifications are sent and a successful push notification followed. Note that when a push notification is sent, it’s also transmitted to each registered device, which may result in a slightly skewed threshold used below.
#Vendor="okta" #event.kind="event" #event.module="sso"
| event.action=~in(values=["system.push.send_factor_verify_push", "user.mfa.okta_verify.deny_push", "user.authentication.auth_via_mfa"])
| case {
Vendor.outcome.result=/fail|deny/i
| mfa_failure_time := @timestamp;
* | mfa_success_time := @timestamp;
}
| case{
Vendor.debugContext.debugData.threatSuspected="true";
Vendor.securityContext.isProxy="true";
Vendor.debugContext.debugData.behaviors=/(New\sDevice|Device\sEval)\=POSITIVE\,\s(New\sIP|IP\sEval)\=POSITIVE/;
Vendor.debugContext.debugData.logOnlySecurityData=/reasons\"\:\"[^\"]+|level\"\:\"(?:high|medium|crit)|(?:(?:velocity|new\s+(?:geo\-location|city|device|ip|state|country))\"\:\"(?:POSITIVE|UNKNOWN))/i;
Vendor.debugContext.debugData.behaviors!=*;
}
| groupBy([user.name, Vendor.authenticationContext.externalSessionId],
function=[
count(mfa_failure_time, as="fail_count"),
count(mfa_success_time, as="success_count"),
selectLast(mfa_failure_time),
selectLast(mfa_success_time)
]
)
| fail_count >= 5 success_count >= 1
| test(mfa_success_time > mfa_failure_time)
| test(fail_count > success_count)
Post-Compromise Attack Vectors
Adversaries employ post-compromise attack vectors after they obtain an initial foothold in the organization. Though we aim to stop breaches at the perimeter of organizations, being able to detect attacks anywhere within the kill chain is vital for defense-in-depth and identifying breaches that may have slipped past perimeter defenses.
The following section will cover persistence mechanisms related to Okta Terrify and lateral movement techniques that abuse Okta’s delegate authority configurations.
Okta Terrify
Okta Terrify is a tool designed to enable persistence to a compromised user’s Okta account. An adversary achieves this persistence by first stealing the compromised user’s DataStore.db database from %LocalAppData%\Okta\OktaVerify\DataStore.db.
The adversary will need to run Okta Terrify on their attack host and run OktaInk.exe from the compromised user’s workstation. Okta Terrify works by dumping information from the stolen DataStore.db database and performing the functionality of Okta Verify from the adversary’s attack host.
The adversary steals sensitive data from the compromised user’s workstation and passes that information to Okta Terrify on the attack host. When executed, Okta Terrify will launch a browser session pointing to the compromised user’s Okta user portal, where the adversary logs in as the compromised user. Okta Terrify will then siphon out data from the interaction. The data is passed to OktaInk.exe — a helper tool for the Okta Terrify toolkit used to sign JSON Web Tokens (JWTs) and steal encryption keys from the compromised user’s workstation — on the compromised user’s host. The adversary uses OktaInk to sign Device Bind JWTs, which are passed back to the attack host with Okta Terrify. Okta Terrify then facilitates the rest of the compromised user’s login session.
Because Okta Terrify has been used to create a fake device bind key with Okta, the adversary’s attack host can be used to continuously log in to the compromised user’s account without the need to maintain access to the compromised user’s workstation.
Identifying Okta Terrify in Use
When executing Okta Terrify, we typically noticed that two user.authentication.auth_via_mfa events were recorded, with different APIs being hit: One to grab a nonce and another to collect the crypto challenge information needed to be imported into OktaInk. Once the OktaInk phase is executed on the victim host, the adversary will have a signed JWT created from the database key and DataStore.db details stored on the victim system. This second request to submit the JWT is an abnormal step in the Okta Verify sequence, offering defenders a detection opportunity.
The following CRT identifies the two distinct user.authentication.auth_via_mfa events that occur within the same session with the corresponding properties. Since Okta Terrify creates a fake Okta Verify instance, there will be two different enrollment IDs during its authentication steps. The rule accounts for this by counting the number of enrollment IDs within a given session.
#Vendor="okta" #event.module="sso" #event.kind="event"
| array:contains(array="event.category[]", value="authentication")
| event.action="user.authentication.auth_via_mfa"
| groupBy([Vendor.authenticationContext.externalSessionId, user.name],
function=[
count(Vendor.AuthenticatorEnrollment.id, as=enrollmentIdCount, distinct=true),
collect(Vendor.debugContext.debugData.url),
collect(user_agent.original),
collect(Vendor.debugContext.debugData.keyTypeUsedForAuthentication),
collect(Vendor.AuthenticatorEnrollment.detailEntry.methodTypeUsed),
collect(Vendor.AuthenticatorEnrollment.id)
]
)
| Vendor.debugContext.debugData.url = /^\/idp\/authenticators\//mi Vendor.debugContext.debugData.url=/^\/idp\/idx\/identify\?/mi
| Vendor.AuthenticatorEnrollment.detailEntry.methodTypeUsed=/Use Okta FastPass/mi
| Vendor.debugContext.debugData.keyTypeUsedForAuthentication=/PROOF_OF_POSSESSION/mi
| enrollmentIdCount > 1
A lower-fidelity detection opportunity may also lie in the fact that Okta Terrify has a static user agent. The tool’s author does modify the user agent and has been observed changing it in the past to avoid detection, so we suggest monitoring the repo for the new user agent in case the author’s pattern changes. The rule shown below looks for instances where historically default user agents have been observed within your environment.
#event.module="sso" #Vendor="okta"
| case {
user_agent.original=/OktaVerify/ user_agent.original=/WPFDeviceSD/i user_agent.original=/Windows/i user_agent.original=/Microsoft_Corporation\/Virtual_Machine/i;
user_agent.original="OktaVerify/5.1.3.0 WPFDeviceSDK/1.8.0.30 Windows/10.0.22621.3155 Microsoft_Corporation/Virtual_Machine";
}
Account Compromise via Delegate Authority
Some Okta configurations utilize delegate authority to facilitate SSO. Another name for this type of seamless authentication is DesktopSSO. In a completely configured state, a user would log in to their workstation and open a browser to https://org.okta.com. The browser would then trigger a Kerberos request with a local domain controller. The domain controller would verify the user’s credentials and issue a Kerberos ticket to the user’s browser for https://org.okta.com. The browser would take this Kerberos ticket and pass it to https://org.kerberos.okta.com, where Okta would validate the ticket and the user. Once validated, the user would be logged in to their Okta portal.
Compromising an account via delegate authority requires the organization to be enrolled in delegate authority within the Okta admin portal and requires this enrollment to be active. Additionally, a service principal name (SPN) must be configured to handle the authentication.
To outline an example attack, we can hypothesize that a user logs in to their workstation and browses to their organization’s Okta portal, such as https://org.okta.com. The browser, which is configured to utilize integrated Windows authentication, makes a request to the network’s key distribution center (KDC) and obtains a Kerberos ticket for org.kerberos.okta.com. The browser sends this ticket to the *.kerberos.okta.com endpoint over HTTPS, which is received and validated, and the user is redirected to https://org.okta.com to be logged in. If the user has MFA methods enabled on their account, the user will be required to submit the MFA token or push notification to log in to their Okta portal. This is an important distinction because the attack described here will fail if the adversary does not have access to the user’s MFA method.
The next step for an adversary is to request a Kerberos service ticket using common attack tools like those from Impacket. This is one area where Falcon Identity Threat Protection could provide attack detection and tool/technique identification as an early warning of the attack in addition to blocking or challenging for access. Going unnoticed or unchallenged, an adversary can obtain a foothold within an organization using tools like Impacket’s GetST.py
or ticketer.py
to obtain a Kerberos ticket for a compromised user:
$ getST.py -spn HTTP/org.kerberos.okta.com -dc-ip 1.2.3.4 LAB/testuser
With the user’s Okta Kerberos ticket, the adversary can leverage Mimikatz to load the user’s ticket into memory on the compromised user’s workstation. This is another area that can be detected in Identity Threat Protection with the adversary passing the ticket, as shown in the image below.
Leave a Reply