Detection rules › Splunk

PingID New MFA Method After Credential Reset

Status
production
Group by
action, object, src, user
Source
github.com/splunk/security_content

MITRE ATT&CK coverage

Event coverage

Rule body splunk

name: PingID New MFA Method After Credential Reset
id: 2fcbce12-cffa-4c84-b70c-192604d201d0
version: 9
creation_date: '2023-12-20'
modification_date: '2026-05-13'
author: Steven Dick
status: production
type: TTP
description: The following analytic identifies the provisioning of a new MFA device shortly after a password reset. It detects this activity by correlating Windows Event Log events for password changes (EventID 4723, 4724) with PingID logs indicating device pairing. This behavior is significant as it may indicate a social engineering attack where a threat actor impersonates a valid user to reset credentials and add a new MFA device. If confirmed malicious, this activity could allow an attacker to gain persistent access to the compromised account, bypassing traditional security measures.
data_source:
    - PingID
search: "`pingid` \"result.message\" = \"*Device Paired*\" | rex field=result.message \"Device (Unp)?(P)?aired (?<device_extract>.+)\" | eval src = coalesce('resources{}.ipaddress','resources{}.devicemodel'), user = upper('actors{}.name'), reason = 'result.message' | eval object=CASE(ISNOTNULL('resources{}.devicemodel'),'resources{}.devicemodel',true(),device_extract) | eval action=CASE(match('result.message',\"Device Paired*\"),\"created\",match('result.message', \"Device Unpaired*\"),\"deleted\") | stats count min(_time) as firstTime, max(_time) as lastTime, values(reason) as reason by src,user,action,object | join type=outer user [| search `wineventlog_security` EventID IN(4723,4724) | eval PW_Change_Time = _time, user = upper(user) | fields user,src_user,EventID,PW_Change_Time] | eval timeDiffRaw = round(lastTime - PW_Change_Time) | eval timeDiff = replace(tostring(abs(timeDiffRaw) ,\"duration\"),\"(\\d*)\\+*(\\d+):(\\d+):(\\d+)\",\"\\2 hours \\3 minutes\") | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `security_content_ctime(PW_Change_Time)` | where timeDiffRaw > 0 AND timeDiffRaw < 3600 | `pingid_new_mfa_method_after_credential_reset_filter`"
how_to_implement: Target environment must ingest Windows Event Log and PingID(PingOne) data sources. Specifically from logs from Active Directory Domain Controllers and JSON logging from a PingID(PingOne) enterprise environment, either via Webhook or Push Subscription.
known_false_positives: False positives may be generated by normal provisioning workflows that generate a password reset followed by a device registration.
references:
    - https://techcommunity.microsoft.com/t5/microsoft-entra-azure-ad-blog/defend-your-users-from-mfa-fatigue-attacks/ba-p/2365677
    - https://www.bleepingcomputer.com/news/security/mfa-fatigue-hackers-new-favorite-tactic-in-high-profile-breaches/
    - https://attack.mitre.org/techniques/T1098/005/
    - https://attack.mitre.org/techniques/T1556/006/
    - https://docs.pingidentity.com/r/en-us/pingoneforenterprise/p14e_subscriptions?tocId=3xhnxjX3VzKNs3SXigWnQA
drilldown_searches:
    - name: View the detection results for - "$user$"
      search: '%original_detection_search% | search  user = "$user$"'
      earliest_offset: $info_min_time$
      latest_offset: $info_max_time$
    - name: View risk events for the last 7 days for - "$user$"
      search: '| from datamodel Risk.All_Risk | search normalized_risk_object IN ("$user$") | stats count min(_time) as firstTime max(_time) as lastTime values(search_name) as "Search Name" values(risk_message) as "Risk Message" values(analyticstories) as "Analytic Stories" values(annotations._all) as "Annotations" values(annotations.mitre_attack.mitre_tactic) as "ATT&CK Tactics" by normalized_risk_object | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`'
      earliest_offset: 7d
      latest_offset: "0"
finding:
    title: An MFA configuration change was detected for [$user$] within [$timeDiff$] of a password reset. The device [$object$] was $action$.
    entity:
        field: user
        type: user
        score: 50
analytic_story:
    - Compromised User Account
    - Scattered Lapsus$ Hunters
asset_type: Identity
mitre_attack_id:
    - T1621
    - T1556.006
    - T1098.005
product:
    - Splunk Enterprise
    - Splunk Enterprise Security
    - Splunk Cloud
category: application
security_domain: access
tests:
    - name: True Positive Test
      attack_data:
        - data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1621/pingid/windows_pw_reset.log
          source: XmlWinEventLog:Security
          sourcetype: XmlWinEventLog
        - data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1621/pingid/pingid.log
          source: PINGID
          sourcetype: _json
      test_type: unit

Stages and Predicates

Stage 1: search

`pingid` "result.message" = "*Device Paired*"

Stage 2: rex

| rex field=result.message "Device (Unp)?(P)?aired (?<device_extract>.+)"

Stage 3: eval

| eval src = coalesce('resources{}.ipaddress','resources{}.devicemodel'), user = upper('actors{}.name'), reason = 'result.message'

Stage 4: eval

| eval object=CASE(ISNOTNULL('resources{}.devicemodel'),'resources{}.devicemodel',true(),device_extract)
object =
ifisnotnull('resources{}.devicemodel')'resources{}.devicemodel'
elsedevice_extract

Stage 5: eval

| eval action=CASE(match('result.message',"Device Paired*"),"created",match('result.message', "Device Unpaired*"),"deleted")
action =
ifmatch('result.message', "Device Paired*")"created"
else"deleted"

Stage 6: stats

| stats count min(_time) as firstTime, max(_time) as lastTime, values(reason) as reason by src,user,action,object

Stage 7: join

| join type=outer user [| search `wineventlog_security` EventID IN(4723,4724) | eval PW_Change_Time = _time, user = upper(user) | fields user,src_user,EventID,PW_Change_Time]

Stage 8: eval

| eval timeDiffRaw = round(lastTime - PW_Change_Time)

Stage 9: eval

| eval timeDiff = replace(tostring(abs(timeDiffRaw) ,"duration"),"(\d*)\+*(\d+):(\d+):(\d+)","\2 hours \3 minutes")

Stage 10: search

| `security_content_ctime(firstTime)`

Stage 11: search

| `security_content_ctime(lastTime)`

Stage 12: search

| `security_content_ctime(PW_Change_Time)`

Stage 13: where

| where timeDiffRaw > 0 AND timeDiffRaw < 3600

Stage 14: search

| `pingid_new_mfa_method_after_credential_reset_filter`

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
"result.message"eq
  • "*Device Paired*"
EventIDin
  • 4723 corpus 2 (splunk 1, kusto 1)
  • 4724
timeDiffRawgt
  • 0
timeDiffRawlt
  • 3600