Persistence · Privilege Escalation
T1098 — Account Manipulation
Adversaries may manipulate accounts — adding credentials, changing permissions, adding delegates or forwarding rules, or modifying group memberships — to maintain persistent access after an initial compromise.
Any unexpected credential, permission, or delegation change on an account should be treated as a potential persistence mechanism established after compromise, especially when it grants broader access or a secondary path back into the account.
Platforms
Windows, Azure AD, Office 365, SaaS
Priority / status
high / complete
Evidence to collect
- Account affected
- Type of change (credential/permission/delegation/group)
- Initiating identity
- Timestamp of change
- Before/after state of the modified attribute
D3-DAM · detect
Domain Account Monitoring
Review role assignments, group membership changes, and OAuth app consent grants for the account.
Tooling: Entra ID, AuditLogs
D3-CR · evict
Credential Revocation
Reset credentials and remove any added authentication methods.
Tooling: Entra ID
D3-MFA · harden
Multi-factor Authentication
Confirm MFA is enforced and no unrecognized authentication method was added.
Tooling: Entra ID, Entra ID
| ATT&CK Technique | D3FEND Technique | Practical SOC Action | Tooling |
|---|---|---|---|
| T1098 Account Manipulation | Domain Account Monitoring | Review role assignments, group membership changes, and OAuth app consent grants for the account. | Entra ID, AuditLogs |
| T1098 Account Manipulation | Credential Revocation | Reset credentials and remove any added authentication methods. | Entra ID |
| T1098 Account Manipulation | Multi-factor Authentication | Confirm MFA is enforced and no unrecognized authentication method was added. | Entra ID, Entra ID |
T1098 Account Manipulation → D3FEND → SOC action
Investigation steps — Microsoft
- Is the path in a temp folder, %APPDATA%, %PROGRAMDATA%, or C:\Users? → Almost certainly malicious
- Is the binary signed? Submit the hash to VirusTotal
- Does the path contain a strange name (random characters, looks like a legitimate system file)?
Investigation steps — generic
- Identify what changed (permission, credential, delegation, group membership) and who initiated it.
- Determine whether the change was authorized through a normal change process.
- Check whether the change grants broader or secondary access.
- Real detection reference: "Service Principal Credential Added for Persistence" -- Replaces the duplicated OAuth consent rule with a cloud-persistence detector for new credentials, secrets, or certificates added to applications/service principals.
Response actions
| Action | Risk | Automation safe | Approval required |
|---|---|---|---|
| Delete the malicious run key via MDE Live Response | Low | No | Yes |
| Quarantine or delete the pointed binary (SHA256 block in MDE | Medium | No | Yes |
| Run full AV scan on the device | Low | No | Yes |
| Check for additional persistence: scheduled tasks, services, | Low | Yes | No |
| Trace the initiating process back to determine how the malwa | Low | No | Yes |
KQL
GEN-PS-001 — Service Principal Credential Added for Persistence
let lookback = 1d;
let credentialOperations = dynamic([
"Add service principal credentials",
"Update service principal",
"Add application",
"Update application",
"Add key credential to application",
"Add password credential to application",
"Add service principal delegated permission grant"
]);
let highRiskTerms = dynamic(["keyCredentials", "passwordCredentials", "servicePrincipal", "federatedIdentityCredentials", "AppRoleAssignment"]);
AuditLogs
| where TimeGenerated >= ago(lookback)
| where OperationName has_any (credentialOperations)
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
AppDisplayName = tostring(TargetResources[0].displayName),
TargetId = tostring(TargetResources[0].id),
ModifiedProperties = tostring(TargetResources[0].modifiedProperties),
Details = tostring(AdditionalDetails)
| where ModifiedProperties has_any (highRiskTerms) or Details has_any (highRiskTerms) or OperationName has_any ("credential", "permission grant")
| project
TimeGenerated,
Actor,
AppDisplayName,
TargetId,
OperationName,
Result,
ModifiedProperties,
Details
| extend timestamp = TimeGenerated,
AccountCustomEntity = Actor
| order by TimeGenerated desc GEN-PE-001 — Rapid Privileged Role Assignments by Actor
let lookback = 1d;
let assignmentThreshold = 3;
AuditLogs
| where TimeGenerated >= ago(lookback)
| where OperationName in~ ("Add member to role", "Add eligible member to role")
| extend InitiatedByUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetRole = tostring(TargetResources[0].displayName)
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| where isnotempty(InitiatedByUPN)
| summarize
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
RoleAssignmentCount = count(),
Roles = make_set(TargetRole, 20),
TargetUsers = make_set(TargetUser, 50)
by InitiatedByUPN
| where RoleAssignmentCount >= assignmentThreshold
| extend AccountName = tostring(split(InitiatedByUPN, "@")[0]),
UPNSuffix = tostring(split(InitiatedByUPN, "@")[1])
| project StartTime, EndTime, InitiatedByUPN, AccountName, UPNSuffix,
RoleAssignmentCount, Roles, TargetUsers
| extend timestamp = StartTime, AccountCustomEntity = InitiatedByUPN
| order by RoleAssignmentCount desc 979c42dd-533e-4ede-b18b-31a84ba8b3d6 — DSRM Account Abuse
Event
| where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
| parse EventData with * 'ProcessId">' ProcessId "<"* 'Image">' Image "<" * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" * 'User">' User "<" *
| where TargetObject has ("HKLM\\System\\CurrentControlSet\\Control\\Lsa\\DsrmAdminLogonBehavior") and Details == "DWORD (0x00000002)"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ProcessId, Image, TargetObject, Details, _ResourceId
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(User, "\\")[1]), AccountNTDomain = tostring(split(User, "\\")[0])
| extend ImageFileName = tostring(split(Image, "\\")[-1])
| extend ImageDirectory = replace_string(Image, ImageFileName, "")
| project-away DomainIndex Escalation criteria
- Change grants privileged or admin-level access.
- Change adds a new authentication method not recognized by the user.
- Mailbox forwarding rule added to an external address.
- Change was made outside the normal change management process.
False positive considerations
- Software installer creating a legitimate startup entry — Add the signed installer's SHA256 to the ApprovedInstallers watchlist
- Browser or application auto-updater — Add specific installer executables to the exclusion list in the query — verify they are from known vendor paths
# T1098 - Account Manipulation
## SOC Recommendation
Any unexpected credential, permission, or delegation change on an account should be treated as a potential persistence mechanism established after compromise, especially when it grants broader access or a secondary path back into the account.
## D3FEND Mappings
| D3FEND Technique | Relationship | Practical SOC Action |
|---|---|---|
| Domain Account Monitoring | Detect | Review role assignments, group membership changes, and OAuth app consent grants for the account. |
| Credential Revocation | Evict | Reset credentials and remove any added authentication methods. |
| Multi-factor Authentication | Harden | Confirm MFA is enforced and no unrecognized authentication method was added. |
## Investigation Steps
1. Is the path in a temp folder, %APPDATA%, %PROGRAMDATA%, or C:\Users? → Almost certainly malicious
2. Is the binary signed? Submit the hash to VirusTotal
3. Does the path contain a strange name (random characters, looks like a legitimate system file)?
## Evidence to Collect
- Account affected
- Type of change (credential/permission/delegation/group)
- Initiating identity
- Timestamp of change
- Before/after state of the modified attribute
## Response Actions
| Action | Risk | Automation Safe | Approval Required |
|---|---|---|---|
| Delete the malicious run key via MDE Live Response | Low | No | Yes |
| Quarantine or delete the pointed binary (SHA256 block in MDE | Medium | No | Yes |
| Run full AV scan on the device | Low | No | Yes |
| Check for additional persistence: scheduled tasks, services, | Low | Yes | No |
| Trace the initiating process back to determine how the malwa | Low | No | Yes |
## KQL
```kql
let lookback = 1d;
let credentialOperations = dynamic([
"Add service principal credentials",
"Update service principal",
"Add application",
"Update application",
"Add key credential to application",
"Add password credential to application",
"Add service principal delegated permission grant"
]);
let highRiskTerms = dynamic(["keyCredentials", "passwordCredentials", "servicePrincipal", "federatedIdentityCredentials", "AppRoleAssignment"]);
AuditLogs
| where TimeGenerated >= ago(lookback)
| where OperationName has_any (credentialOperations)
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
AppDisplayName = tostring(TargetResources[0].displayName),
TargetId = tostring(TargetResources[0].id),
ModifiedProperties = tostring(TargetResources[0].modifiedProperties),
Details = tostring(AdditionalDetails)
| where ModifiedProperties has_any (highRiskTerms) or Details has_any (highRiskTerms) or OperationName has_any ("credential", "permission grant")
| project
TimeGenerated,
Actor,
AppDisplayName,
TargetId,
OperationName,
Result,
ModifiedProperties,
Details
| extend timestamp = TimeGenerated,
AccountCustomEntity = Actor
| order by TimeGenerated desc
```
```kql
let lookback = 1d;
let assignmentThreshold = 3;
AuditLogs
| where TimeGenerated >= ago(lookback)
| where OperationName in~ ("Add member to role", "Add eligible member to role")
| extend InitiatedByUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetRole = tostring(TargetResources[0].displayName)
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| where isnotempty(InitiatedByUPN)
| summarize
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
RoleAssignmentCount = count(),
Roles = make_set(TargetRole, 20),
TargetUsers = make_set(TargetUser, 50)
by InitiatedByUPN
| where RoleAssignmentCount >= assignmentThreshold
| extend AccountName = tostring(split(InitiatedByUPN, "@")[0]),
UPNSuffix = tostring(split(InitiatedByUPN, "@")[1])
| project StartTime, EndTime, InitiatedByUPN, AccountName, UPNSuffix,
RoleAssignmentCount, Roles, TargetUsers
| extend timestamp = StartTime, AccountCustomEntity = InitiatedByUPN
| order by RoleAssignmentCount desc
```
```kql
Event
| where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
| parse EventData with * 'ProcessId">' ProcessId "<"* 'Image">' Image "<" * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" * 'User">' User "<" *
| where TargetObject has ("HKLM\\System\\CurrentControlSet\\Control\\Lsa\\DsrmAdminLogonBehavior") and Details == "DWORD (0x00000002)"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ProcessId, Image, TargetObject, Details, _ResourceId
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(User, "\\")[1]), AccountNTDomain = tostring(split(User, "\\")[0])
| extend ImageFileName = tostring(split(Image, "\\")[-1])
| extend ImageDirectory = replace_string(Image, ImageFileName, "")
| project-away DomainIndex
```
## Escalation Criteria
- Change grants privileged or admin-level access.
- Change adds a new authentication method not recognized by the user.
- Mailbox forwarding rule added to an external address.
- Change was made outside the normal change management process.
## False Positive Considerations
- Software installer creating a legitimate startup entry — Add the signed installer's SHA256 to the ApprovedInstallers watchlist
- Browser or application auto-updater — Add specific installer executables to the exclusion list in the query — verify they are from known vendor paths
Generated by SOC Response Atlas by Basyrix.
Want to push this directly to Confluence? Upgrade to Basyrix Pro.
- /api/techniques/T1098.json
- /api/recommendations/T1098.json
- /api/d3fend/T1098.json
- /api/mappings/T1098.json
- /api/confluence/T1098.md
Example curl:
curl https://atlas.basyrix.com/api/recommendations/T1098.json Response:
{
"technique_id": "T1098",
"name": "Account Manipulation",
"priority": "high",
"status": "complete",
"version": "0.1.0",
"last_reviewed": "2026-07-23",
"generated_by": "SOC Response Atlas by Basyrix",
"tactics": [
"Persistence",
"Privilege Escalation"
],
"platforms": [
"Windows",
"Azure AD",
"Office 365",
"SaaS"
],
"summary": "Adversaries may manipulate accounts — adding credentials, changing permissions, adding delegates or forwarding rules, or modifying group memberships — to maintain persistent access after an initial compromise.\n",
"soc_recommendation": "Any unexpected credential, permission, or delegation change on an account should be treated as a potential persistence mechanism established after compromise, especially when it grants broader access or a secondary path back into the account.\n",
"d3fend_mappings": [
{
"id": "D3-DAM",
"name": "Domain Account Monitoring",
"relationship": "detect",
"practical_action": "Review role assignments, group membership changes, and OAuth app consent grants for the account.\n",
"tooling": [
"Entra ID",
"AuditLogs"
]
},
{
"id": "D3-CR",
"name": "Credential Revocation",
"relationship": "evict",
"practical_action": "Reset credentials and remove any added authentication methods.",
"tooling": [
"Entra ID"
]
},
{
"id": "D3-MFA",
"name": "Multi-factor Authentication",
"relationship": "harden",
"practical_action": "Confirm MFA is enforced and no unrecognized authentication method was added.",
"tooling": [
"Entra ID",
"Entra ID"
]
}
],
"investigation_steps": {
"microsoft": [
"Is the path in a temp folder, %APPDATA%, %PROGRAMDATA%, or C:\\Users? → Almost certainly malicious",
"Is the binary signed? Submit the hash to VirusTotal",
"Does the path contain a strange name (random characters, looks like a legitimate system file)?"
],
"generic": [
"Identify what changed (permission, credential, delegation, group membership) and who initiated it.",
"Determine whether the change was authorized through a normal change process.",
"Check whether the change grants broader or secondary access.",
"Real detection reference: \"Service Principal Credential Added for Persistence\" -- Replaces the duplicated OAuth consent rule with a cloud-persistence detector for new credentials, secrets, or certificates added to applications/service principals."
]
},
"evidence_to_collect": [
"Account affected",
"Type of change (credential/permission/delegation/group)",
"Initiating identity",
"Timestamp of change",
"Before/after state of the modified attribute"
],
"response_actions": [
{
"name": "Delete the malicious run key via MDE Live Response",
"category": "Response",
"risk": "Low",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Endpoint"
},
{
"name": "Quarantine or delete the pointed binary (SHA256 block in MDE",
"category": "Response",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Endpoint",
"notes": "Quarantine or delete the pointed binary (SHA256 block in MDE custom indicators)"
},
{
"name": "Run full AV scan on the device",
"category": "Response",
"risk": "Low",
"automation_safe": false,
"approval_required": true,
"tool": "See investigation guide"
},
{
"name": "Check for additional persistence: scheduled tasks, services,",
"category": "Response",
"risk": "Low",
"automation_safe": true,
"approval_required": false,
"tool": "See investigation guide",
"notes": "Check for additional persistence: scheduled tasks, services, WMI subscriptions"
},
{
"name": "Trace the initiating process back to determine how the malwa",
"category": "Response",
"risk": "Low",
"automation_safe": false,
"approval_required": true,
"tool": "See investigation guide",
"notes": "Trace the initiating process back to determine how the malware was initially delivered"
}
],
"queries": {
"kql": [
{
"name": "GEN-PS-001 — Service Principal Credential Added for Persistence",
"description": "Replaces the duplicated OAuth consent rule with a cloud-persistence detector for new credentials, secrets, or certificates added to applications/service principals. (Source: Bell Integration baseline detection library, mapped via sub-technique T1098.003.)",
"query": "let lookback = 1d;\nlet credentialOperations = dynamic([\n \"Add service principal credentials\",\n \"Update service principal\",\n \"Add application\",\n \"Update application\",\n \"Add key credential to application\",\n \"Add password credential to application\",\n \"Add service principal delegated permission grant\"\n]);\nlet highRiskTerms = dynamic([\"keyCredentials\", \"passwordCredentials\", \"servicePrincipal\", \"federatedIdentityCredentials\", \"AppRoleAssignment\"]);\nAuditLogs\n| where TimeGenerated >= ago(lookback)\n| where OperationName has_any (credentialOperations)\n| extend Actor = tostring(InitiatedBy.user.userPrincipalName),\n AppDisplayName = tostring(TargetResources[0].displayName),\n TargetId = tostring(TargetResources[0].id),\n ModifiedProperties = tostring(TargetResources[0].modifiedProperties),\n Details = tostring(AdditionalDetails)\n| where ModifiedProperties has_any (highRiskTerms) or Details has_any (highRiskTerms) or OperationName has_any (\"credential\", \"permission grant\")\n| project\n TimeGenerated,\n Actor,\n AppDisplayName,\n TargetId,\n OperationName,\n Result,\n ModifiedProperties,\n Details\n| extend timestamp = TimeGenerated,\n AccountCustomEntity = Actor\n| order by TimeGenerated desc"
},
{
"name": "GEN-PE-001 — Rapid Privileged Role Assignments by Actor",
"description": "Flags actors assigning multiple privileged roles in a short period. (Source: Bell Integration baseline detection library.)",
"query": "let lookback = 1d;\nlet assignmentThreshold = 3;\nAuditLogs\n| where TimeGenerated >= ago(lookback)\n| where OperationName in~ (\"Add member to role\", \"Add eligible member to role\")\n| extend InitiatedByUPN = tostring(InitiatedBy.user.userPrincipalName)\n| extend TargetRole = tostring(TargetResources[0].displayName)\n| extend TargetUser = tostring(TargetResources[0].userPrincipalName)\n| where isnotempty(InitiatedByUPN)\n| summarize\n StartTime = min(TimeGenerated),\n EndTime = max(TimeGenerated),\n RoleAssignmentCount = count(),\n Roles = make_set(TargetRole, 20),\n TargetUsers = make_set(TargetUser, 50)\n by InitiatedByUPN\n| where RoleAssignmentCount >= assignmentThreshold\n| extend AccountName = tostring(split(InitiatedByUPN, \"@\")[0]),\n UPNSuffix = tostring(split(InitiatedByUPN, \"@\")[1])\n| project StartTime, EndTime, InitiatedByUPN, AccountName, UPNSuffix,\n RoleAssignmentCount, Roles, TargetUsers\n| extend timestamp = StartTime, AccountCustomEntity = InitiatedByUPN\n| order by RoleAssignmentCount desc"
},
{
"name": "979c42dd-533e-4ede-b18b-31a84ba8b3d6 — DSRM Account Abuse",
"description": "This query detects an abuse of the DSRM account in order to maintain persistence and access to the organization's Active Directory. Ref: https://adsecurity.org/?p=1785' (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed).)",
"query": "Event\n| where EventLog == \"Microsoft-Windows-Sysmon/Operational\" and EventID in (13)\n| parse EventData with * 'ProcessId\">' ProcessId \"<\"* 'Image\">' Image \"<\" * 'TargetObject\">' TargetObject \"<\" * 'Details\">' Details \"<\" * 'User\">' User \"<\" * \n| where TargetObject has (\"HKLM\\\\System\\\\CurrentControlSet\\\\Control\\\\Lsa\\\\DsrmAdminLogonBehavior\") and Details == \"DWORD (0x00000002)\"\n| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ProcessId, Image, TargetObject, Details, _ResourceId\n| extend HostName = tostring(split(Computer, \".\")[0]), DomainIndex = toint(indexof(Computer, '.'))\n| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)\n| extend AccountName = tostring(split(User, \"\\\\\")[1]), AccountNTDomain = tostring(split(User, \"\\\\\")[0])\n| extend ImageFileName = tostring(split(Image, \"\\\\\")[-1])\n| extend ImageDirectory = replace_string(Image, ImageFileName, \"\")\n| project-away DomainIndex"
}
],
"spl": [
{
"name": "Account modification events",
"description": "Review account modification events in Splunk.",
"query": "index=audit action=\"account_modified\" user=\"<user>\" earliest=-24h\n"
}
],
"esql": [
{
"name": "696015ef-718e-40ff-ac4a-cc2ba88dbeeb — AWS IAM User Created Access Keys For Another User",
"description": "(ESQL) An adversary with access to a set of compromised credentials may attempt to persist or escalate privileges by creating a new set of credentials for an existing user. This rule looks for use of the IAM `CreateAccessKey` API operation to create new programmatic access keys for another IAM user. (Source: Elastic's official detection-rules repository (Elastic License v2).)",
"query": "from logs-aws.cloudtrail-* metadata _id, _version, _index\n| where data_stream.dataset == \"aws.cloudtrail\"\n and event.provider == \"iam.amazonaws.com\"\n and event.action == \"CreateAccessKey\"\n and event.outcome == \"success\"\n and user.name != user.target.name\n| keep\n @timestamp,\n cloud.account.id,\n cloud.region,\n event.provider,\n event.action,\n event.outcome,\n data_stream.dataset,\n user.name,\n source.address,\n source.ip,\n user.target.name,\n user_agent.original,\n aws.cloudtrail.request_parameters,\n aws.cloudtrail.response_elements,\n aws.cloudtrail.user_identity.arn,\n aws.cloudtrail.user_identity.type,\n aws.cloudtrail.user_identity.access_key_id,\n source.geo.*,\n _id,\n _version,\n _index"
},
{
"name": "71159cad-f8b3-4b3c-ac6e-9cab05594670 — AWS IAM Credentials Added to a Bedrock API Key Phantom User",
"description": "(ESQL) Identifies standard IAM credentials being added to an Amazon Bedrock API key phantom user, whose user name starts with \"BedrockAPIKey-\": either a long-term access key (CreateAccessKey) or a console password / login profile (CreateLoginProfile, UpdateLoginProfile)... (Source: Elastic's official detection-rules repository (Elastic License v2).)",
"query": "FROM logs-aws.cloudtrail-* METADATA _id, _version, _index\n| WHERE event.provider == \"iam.amazonaws.com\"\n AND event.action IN (\"CreateAccessKey\", \"CreateLoginProfile\", \"UpdateLoginProfile\")\n AND event.outcome == \"success\"\n AND user.target.name LIKE \"BedrockAPIKey-*\"\n| KEEP _id, _version, _index, @timestamp, aws.*, cloud.*, event.*, source.*, user.*, user_agent.*"
}
]
},
"automation": {
"safe": [
"Add recommendation as Sentinel incident comment.",
"Remove a confirmed-malicious added MFA method or app consent grant.",
"Create ServiceNow SecOps task."
],
"approval_required": [
"Revert a role/group membership change.",
"Reset credentials."
]
},
"escalation_criteria": [
"Change grants privileged or admin-level access.",
"Change adds a new authentication method not recognized by the user.",
"Mailbox forwarding rule added to an external address.",
"Change was made outside the normal change management process."
],
"false_positive_considerations": [
"Software installer creating a legitimate startup entry — Add the signed installer's SHA256 to the ApprovedInstallers watchlist",
"Browser or application auto-updater — Add specific installer executables to the exclusion list in the query — verify they are from known vendor paths"
],
"confluence": {
"title": "T1098 - Account Manipulation Response Guidance",
"labels": [
"mitre",
"attack",
"d3fend",
"secops",
"basyrix"
],
"sections": [
"summary",
"d3fend_mappings",
"investigation_steps",
"response_actions",
"queries",
"automation",
"escalation_criteria",
"false_positive_considerations"
]
}
}