SOQL injection in SalesForce earned me $$$$$
Have you ever exploited a database that doesn’t have tables!
Introduction
SOQL (Salesforce Object Query Language), is essential for querying data within Salesforce environments. However, if improperly handled, SOQL can be vulnerable to injection attacks, where an attacker can manipulate queries to access unauthorized data. In this write-up, i’ll dive into a real-world example of SOQL injection, explore why it happens, and How i was able to get a full impact from it
The Story: (skip it if you want as it’s not related to technical details)
Some weeks ago, I was bored from sitting at my desk for almost 8 hours a day, so I decided to take my Mac and go outdoors to drink something,so I went to a cafe, got my laptop, and decided to read about something new. During those days, I was targeting a site that runs Salesforce CRM, which was using Salesforce Lightning (Aura) with long encoded requests that I didn’t understand. So, I started reading about Aura.
While I was reading, taking notes, and trying to exploit it on a target (not the one where I found the SOQL injection), I attempted to get some IDORs from it. However, after drinking my coffee, I found no results except some notes about Aura and Apex. A few weeks later, while hunting on Synack, I decided to search for targets that use Salesforce to see if I’d learned something new — and I think I did xD. This target was also using Aura, so I spent some time on it. While hunting, I found that some requests returned errors when entering single quotes in the Aura request parameters. I’m not a Salesforce expert, so I thought, Is that SQL injection?
SQLmap, as usual, said ‘not vulnerable,’ but cmon when has SQLmap ever been useful for me? 😁 After some Googling, I found articles on SOQL, especially from the legend Jason Haddix, who mentioned it in a tweet. That was a sign SQL could be possible here, but how? I couldn’t get any traditional SQL injection payloads to work.
SQLmap doesn’t even support SOQL injection until now Reference
So as always ..
If you want to know what is really going on, Go straight to the source
So i went to SalesForce Documentation and started learning new things.. until I finished up with 3 SOQL injections that earned me thousands of dollars 💵
Technical details
Let’s have a high-level overview on some definitions we will need them afterwards
- SalesForce: is a cloud-based Customer Relationship Management (CRM) platform that helps businesses manage customer data, interactions, and business processes. It offers a variety of tools for sales, marketing, customer service, and application development
- aura: is a UI framework developed by Salesforce to build dynamic web applications for mobile and desktop devices. It’s part of Salesforce’s Lightning component framework.
- APEX: is Salesforce’s proprietary programming language, similar to Java. It is used for implementing business logic on Salesforce’s servers.
- SOQL: is Salesforce’s query language used to retrieve data from the Salesforce database. It’s similar to SQL but adapted to work with Salesforce’s data model
Understanding SOQL and Its Unique Structure
Before diving into the vulnerability, let’s look at some of the unique characteristics of SOQL compared to traditional SQL.
1. No Multi-Query Support
Unlike SQL, SOQL does not allow multiple queries in a single request, so attackers cannot leverage stacked queries as they might in traditional SQL injection attacks.
2. Lack of Comment Support
SOQL doesn’t support comments (like — or /* */ in SQL). This means i can’t use comments to truncate or modify parts of a query,
3. Object-Oriented Structure Instead of Tables
Salesforce is built around objects rather than tables. Each Salesforce object represents a CRM entity, (Sobjects)
such as Account, Contact, or User, and contains specific fields that store various data attributes.
Salesforce Objects and Fields
Salesforce objects come with two types of fields:
• Default Fields: Predefined by Salesforce, such as Name, CreatedDate, and OwnerId.
• Custom Fields: Defined by developers to meet specific business needs, often with a (__c )suffix. For custom objects and (__r) for custom relationships to distinguish them from default fields.
For example, an Account object might include:
• Default fields: AccountName, AccountNumber and Industry
• Custom fields: Customer_Type__c, Referral_Source__c,blablabla__c or even customer_password__c
This structure is fundamental to Salesforce and is central to how SOQL queries retrieve data from objects instead of traditional database tables.
Enough talking let’s Hack in 💵
The target was an authenticated assessment after login with the credentials. The first thing I look for is the user dashboard. On another monitor, Burp is running, and I saw the Aura endpoint /s/sfsites/aura? being requested. The Aura request contains 3 main parameters.
- Aura message (where the play take place )
- Aura Context
- Aura token
we are intersted in Aura message as it is the paramter that contains the request to the server
In the Aura message, the request was sent in JSON format and URL-encoded. it is a messy to read by burp inspector the request look like this
%7B%22id%22%3A%22239%3Ba%22%2C%22descriptor%22%3A%22aura%3A%2F%2FApexActionController%2FACTION%24execute%22%2C%22callingDescriptor%22%3A%22UNKNOWN%22%2C%22params%22%3A%7B%22namespace%22%3A%22%22%2C%22classname%22%3A%22Redacted_data%22%2C%22method%22%3A%22loadTableData%22%2C%22params%22%3A%7B%22config%22%3A%22Manage+Users%22%2C%22portalType%22%3A%22redated_data%22%2C%22selectedAccount%22%3A%22123456789%22%2C%22sortField%22%3A%22%22%2C%22sortOrder%22%3A%22%22%2C%22rowLimit%22%3A%22%22%2C%22refresh%22%3Atrue%2C%22firstName%22%3A%22%22%2C%22lastName%22%3A%22%22%2C%22Status%22%3A%22%22%2C%22roleName%22%3A%22%22%2C%22viewType%22%3A%22%22%2C%22brandUsers%22%3Afalse%7D%2C%22cacheable%22%3Afalse%2C%22isContinuation%22%3Afalse%7D%7D%0A
In the UI, there was a radio button to filter for active/inactive users; toggling this button sends another POST request to the LoadTableData method.
This caught my eye 👀 — table and data?
Using Hackvertor, I decoded it on the fly and began testing
After decoding it from URL encoding, I could see the request more clearly. The most important part is the “params”, which include fields for FirstName, LastName, and Status. The first two fields take string values, while Status takes a boolean.
“FirstName”:”rooted” this is a true condition as my account already named rooted
"FirstName”:”Wrong” results in empty output, as there is no user has FirstName “wrong”
“FirstName”:”R” results in return of all accounts that have ‘r’ char , (regex match)
so as long as it perform regex match
I began by injecting my favorite character ('
) and performed some initial checks.
query in backend may look something like this
List<Account> accounts = [SELECT Id, Name, FirstName FROM Account WHERE FirstName LIKE '%rooted%'];
normal payload will be break out from the like operator and from the string and then balance the query i was thinking in something like this
%' or 1=1 --
P.S. Using OR is not the safest way to prove a SQLi vulnerability, as it can damage the database and introduce risks. I used it in this case because I know the query is for retrieving data — so if it works, data will be returned; if not, nothing happens. However, injecting into DELETE, UPDATE, or INSERT statements can overwrite entire tables. Be careful, Hecker! :-)
so it becomes
List<Account> accounts = [SELECT Id, Name, FirstName FROM Account WHERE FirstName LIKE '%%' or 1=1 --%'
however i got errors
Then I realized I couldn’t use OR conditions in this query, as I was limited to AND. Additionally, SOQL doesn’t evaluate numerical values like 1=1; it requires data fields + no comments supprot
so i have to balance the query with known fields , as LastName
"firstname":"%' and LastName != 'NotEvenExists
= TRUE
"firstname":"%' and LastName = 'NotEvenExists
= FALSE
it should look like this in the backend
List<Account> accounts = [SELECT Id, Name, FirstName FROM Account WHERE FirstName LIKE '%%' and LastName != 'NotEvenExists%'
Since we can’t use comments (-- or #), we leave ‘NotEvenExists without a closing quote (who wants comments to do SQL Injection? )
FlashPoint to the Past
as we already know object USER has default filed called FirstName
so, Lets’ try to reference the object before the filed
"FirstName”:”rooted%’ and User.LastName = ‘%synack” = TRUE !!
"FirstName”:”rooted%’ and User.FirstName = ‘%wrong” = FALSE !!
Now that I can prove injection is happening, I could report it and get 50% of the reward. But come on — let’s dig deeper into the Salesforce documentation and find a way to prove the full impact! :))”
we can use regex again but we first must find a good filed to extract data from it
"FirstName":"%' and User.FirstName like 'r%' and User.FirstName != 'blablablablabla" //= TRUE !!
Using Like Operator to match for regex as strings is a funny way to extract data by Fuzzing char by char , the part User.FirstName like ‘r%’ will match for any string began with r , and if exists it will return true condition
from here we can break the payload two three parts
1. break the statment with %'
2. injection part AND account.name like 'admin%'
3. balance the query and user.firstname != '%blablabla
So, we will ignore part 1, as it’s only used to break into the APEX, and part 3, as it’s only there to balance the query in the backend (there are static parts). I will only modify part 2.
Reveal hidden fields
With the fuzzing, I have dumped all default fields’ data (except for integers like phone numbers due to a casting error with the LIKE operator, which only accepts strings, not numbers. Limits are everywhere in SOQL. :)))
we know the default fields from the documentation
that include phone number, names, address etc.
So, I can access another user’s PII data with:
%’ and account.name = “admin” and account.BillingCity like ‘FUZZCHAR%’ and Lastname != ‘%’
But what about the custom fields?
Let’s brute-force!
As I spent some time with the target, I noticed they use CamelCase naming conventions with underscores (_).
With some fuzzing using Burp Intruder, the custom fields revealed Account.Account_Password__c.
Where Account is known, __c is the suffix for custom fields, and Account_Password were the only fields that required heavy brute-forcing
Final exploitation and dump another users passwords
Knowing the admin name and the custom fields, it was a straightforward brute-force attack, character by character, using regex to find any user passwords.
%' and account.name = "admin" and account.Account_Password__c like 'FUZZCHAR%' and Lastname !='%
Results !
Three accepted SQL injections with full impact, all paid in full.
Conclusion
It was a very limited environment to break into — no comments, no functions, no stacked queries, and no resources available online. However, there is always documentation on how things are built. If you know how to build something, breaking it becomes much easier
Key Takeaways:
1. Understand Salesforce Architecture: SOQL works with objects, not tables, requiring tailored exploitation techniques.
2. Adapt Injection Techniques: Exploiting SOQL demands creative payloads due to its lack of stacked queries, comments, and OR conditions.
3. Exploit Regex Matching: Use the LIKE operator and fuzzing to extract sensitive data like custom fields and user credentials.
4. Leverage Documentation: Deep understanding of Salesforce’s structure is key to finding and exploiting vulnerabilities.
5. Mitigate SOQL Risks: Developers must use parameterized queries and input sanitization to prevent injections.