Breaching Salesforce orgs via Smart Lists by Salesforce Labs

July 2024, by Andrew Schoonmaker

DNYD-2024-03
CVSS: 7.5 - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

While reviewing a popular free Salesforce integration, Smart Lists by Salesforce Labs (solutions designed by Salesforce employees), we came across one of our most interesting finds yet!

As we go over this issue, it’s important to remember that Salesforce Apex runs in the system context except when you explicitly tell it to apply user permissions or query with security or sharing applied. We have all had those clauses hammered into our brains at this point, but it’s still easy to miss when it’s wrapped inside 3 nested function calls. In this blog post, we’ll be detailing how we were able to leverage our understanding of SOQL, the app documentation, and the behavior to achieve a total org compromise.

Salesforce even gave me a shout out for my responsible disclosure once we identified the issue!

After we disclosed the issue, the team very quickly identified the root issue and pushed a fix in 3.5.0 which is already publicly available at the time of publishing this article.

You should be able to follow along with the older version here:
https://appexchange.salesforce.com/appxListingDetail?listingId=a0N4V00000HEZW9UAP
https://login.salesforce.com/?startURL=%2Fpackaging%2FinstallPackage.apexp%3Fp0%3D04t8b000001BVDvAAO%26newUI%3D1%26src%3Du

Luckily I was examining network traffic when I viewed a list utilizing this controller. My eyes have gotten well trained at this point to look for anything with the Salesforce custom suffixes like __c or __mdt in particular and this one caught my attention immediately. Remember, if you can control the object name or ID on an Apex controller, you must at least attempt IDOR and SOQL Injection. In this case, having control of the FROM object was unexpected, and my eyes kept getting bigger when I read the whole request:

{"actions":[{"id":"460;a","descriptor":"aura://ApexActionController/ACTION$execute","callingDescriptor":"UNKNOWN","params":{"namespace":"smartlists","classname":"SmartListController","method":"getPage","params":{"dataSourceType":"SOQL+with+Sharing","dataProviderClass":null,"objectName":"Case_Enhancement__c","listFilter":"","soqlScope":"everything","filterEntries":[],"parentIdField":null,"parentId":null,"queryFields":"Id,Subject","sortField":"Id","sortDirection":"desc","offset":0,"pageSize":20},"cacheable":false,"isContinuation":false}}]}


We can do so much!

  • Control the output size with pageSize
  • Control the returned fields with queryFields
  • Sort by with sortField
  • The WHERE clause is determined by listFilter

Playing with the request, we can see that it’s got some injection issues!

Smart Lists Burp

I changed the objectName Case_Enhancement to EntityDefinition and updated the sort to go on QualifiedApiName since that table does not have an Id field: What do you know? Our introspection results came back. Updating pageSize to 9999 and we were in the money! User mode SOQL queries can be great for listing out things not supported by the Aura API, like NetworkMember and quicker enumeration, but we wanted to keep pushing.

At this point, we did get Salesforce Security on the phone, because unexpectedly being able to read things like Custom Metadata and Custom Settings was troubling.

Smart Lists Burp 2

Smart Lists Burp 2-2

In the above screenshots you can see that discrepancy. But we knew there was something deeper going on here, the data source SOQL with Sharing parameter was really getting to us! So we went back to school. At denEyed, we like to joke that this work really does require a lot of studying and reading different documentations. We even have to set up massive test cases with switch flipping to test our hypotheses in Apex sometimes.

You can read the app documentation on the listing page.

Smart Lists docs

All we needed to do was update the dataSourceType to SOQL without Sharing. All of a sudden the query was returning records that were not visible to our user! Let’s show this in action looking at the Case object. It really is disturbing the kind of information you can pull from other cases. Remember how a breach in Okta’s support system led to a Cloudflare breach?

{"actions":[{"id":"460;a","descriptor":"aura://ApexActionController/ACTION$execute","callingDescriptor":"UNKNOWN","params":{"namespace":"smartlists","classname":"SmartListController","method":"getPage","params":{"dataSourceType":"SOQL+without+Sharing","dataProviderClass":null,"objectName":"Case","listFilter":"","soqlScope":"everything","filterEntries":[],"parentIdField":null,"parentId":null,"queryFields":"Id,Subject,ContactEmail","sortField":"Id","sortDirection":"desc","offset":0,"pageSize":9999},"cacheable":false,"isContinuation":false}}]}


Smart Lists Burp 3

From here, I recommend checking out the EntityDefinition table. You can read anything you want. So go for the crown jewels! You can filter for Custom Settings and Metadata and you might get lucky and find credentials that you didn’t have permission to read before and subsequently escalate your privileges by signing into an API… we might have a blog on this later, keep your eyes open.

You may want to check out our tool AuraQL which can help analyze your org and take inventory of what’s at risk in the case of a massive blast radius like this. It is important to remember that Apex code always runs as system context, so whatever is stored on the system could be accessible if you let the attacker drive!