Skip to content

Lateral Movement Paths

This page documents the attack patterns SFHound is designed to surface. Each path corresponds to one or more traversal queries you can run in BloodHound CE.


The general model

Every lateral movement path in Salesforce follows one of two patterns:

Permission escalation — A user holds or gains a permission that allows them to directly alter the org’s security posture (e.g., ManageProfilesPermissionsets → add ModifyAllData to a Permission Set → assign to self).

Data exfiltration — A user’s effective permissions, resolved through their Profile/PermissionSet/group membership, allow them to read or export data they should not have access to.

The SFHound graph resolves both patterns through traversal queries.


Path 1 — Profile/PermissionSet chain to system permission

The most common path. A user is assigned a Permission Set Group containing a Permission Set that grants a dangerous system permission.

SFUser ──AssignedPermissionSetGroup──► SFPermissionSetGroup
IncludesPermissionSet
SFPermissionSet
ModifyAllData
SFOrganization

Detection query:

MATCH p=(u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps)-[:ModifyAllData|AuthorApex|ManageUsers|ManageProfilesPermissionsets]->(org:SFOrganization)
WHERE (ps:SFProfile OR ps:SFPermissionSet)
RETURN p
LIMIT 500

Path 2 — ManageProfiles self-escalation

A user with ManageProfilesPermissionsets can add ModifyAllData to any Permission Set, then assign that set to themselves.

Compromised user
└── ManageProfilesPermissionsets ──► SFOrganization
Attacker action: edit SFPermissionSet → add ModifyAllData edge
assign SFPermissionSet to self
→ gains ModifyAllData

Detection query — who can manage profiles?

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:ManageProfilesPermissionsets]->(org:SFOrganization)
WHERE ps:SFProfile OR ps:SFPermissionSet
RETURN path

Path 3 — ManageUsers → create admin

A user with ManageUsers can create a new user with a System Administrator profile, or re-activate a dormant admin account.

Detection query:

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:ManageUsers]->(org:SFOrganization)
WHERE ps:SFProfile OR ps:SFPermissionSet
RETURN path

Hardening: Pair with enhanced transaction security policies that alert on UserCreate events by non-admin principals.


Path 4 — AuthorApex → arbitrary code execution

An identity with AuthorApex can deploy a trigger or class that runs in system context, bypassing field-level security and sharing. From there:

  • Create a backdoor admin user
  • Exfiltrate data via HTTPS callout to an attacker-controlled server
  • Delete or overwrite audit records

Detection query:

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps)-[:AuthorApex]->(org:SFOrganization)
WHERE (ps:SFProfile OR ps:SFPermissionSet)
RETURN path

Path 5 — ViewAllData + ApiEnabled → bulk exfiltration

Neither permission alone is necessarily alarming. Together, they allow a service account to extract every record via the REST or Bulk API without triggering row-level sharing checks.

Detection query:

MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps1)-[:ViewAllData]->(org:SFOrganization),
(u)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps2)-[:ApiEnabled]->(org)
WHERE (ps1:SFProfile OR ps1:SFPermissionSet)
AND (ps2:SFProfile OR ps2:SFPermissionSet)
RETURN DISTINCT u.name AS User, u.Username AS Username

Path 6 — CanModifyAll on private object → sharing bypass

Even with a private OWD, a user with CanModifyAll on a private object can read, edit, and delete all records of that type, bypassing role hierarchy and sharing rules entirely.

SFUser ──AssignedPermissionSet──► SFPermissionSet
CanModifyAll
SFSObject (InternalSharingModel: Private)

Detection query:

MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p)-[:CanModifyAll]->(obj:SFSObject)
WHERE obj.InternalSharingModel = "Private"
AND (p:SFPermissionSet OR p:SFProfile)
RETURN DISTINCT u.name AS User, obj.name AS Object, obj.Label, obj.InternalSharingModel
ORDER BY u.name

Path 7 — OAuth Connected App abuse

A user with CanAuthorize on a Connected App that has AdminApprovedUsersOnly = False can self-authorise and obtain a long-lived OAuth token. If the Connected App has permissive scopes (e.g., full), that token grants the effective permissions of the user to any third-party service.

SFUser ──AssignedProfile──► SFProfile
CanAuthorize
SFConnectedApp (AdminApprovedUsersOnly: false)

Detection query:

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet*1..2]->(ps)-[:CanAuthorize]->(app:SFConnectedApp)
WHERE app.AdminApprovedUsersOnly = False
RETURN path

Path 8 — Role hierarchy record visibility escalation

A user assigned to a high position in the role hierarchy can read all records owned by users in any descendant role — even without explicit sharing or ViewAllData.

Detection: who sits at the root of the hierarchy?

MATCH (r:SFRole)
WHERE NOT ()-[:InheritsRole]->(r)
MATCH (u:SFUser)-[:HasRole]->(r)
RETURN u.name, r.Name AS TopRole

Path 9 — Nested Public Group membership

A user added to a low-trust Public Group that is nested inside a high-trust group gains all the sharing-rule visibility and Connected App access of the outer group.

Detection: deep group membership paths

MATCH path = (outer:SFGroup)-[:HasMember*2..5]->(u:SFUser)
RETURN path
LIMIT 200

Chained path example — full escalation story

Compromised service account
└── AssignedPermissionSet → "Integration_Read_Only" PermissionSet
└── ManageProfilesPermissionsets → SFOrganization (misconfigured!)
Attacker edits "Integration_Read_Only":
replaces single scoped permission with ModifyAllData
└── ModifyAllData → SFOrganization
└── Bulk-exports all records via REST API

This exact chain is detectable by traversing all paths from any user to ManageProfilesPermissionsets and checking if the same user also has ApiEnabled.