Salesforce-powered Account Takeover

Swehtpantz
8 min readJan 4, 2024

--

https://www.sangfroidwebdesign.com/resources/what-to-do-clicked-phishing-link/

Disclaimer: Please use this information for educational purposes only. I do not support illegal activity, but am fascinated as to how web application vulnerabilities arise and strive to assist companies in securing themselves and their customers.

TL;DR - I found a Reflected XSS that allowed me to import malicious JS that stole a token from local storage and queried an endpoint for an account ID. With these two pieces of data, I had all I required to takeover/delete a victim’s account with a single phishing link click.

Summary

I was recently hacking on an web application that is mainly used for medical professionals, and one of the technologies it uses is Salesforce.

The exploit chain is broken down in a few several areas:

  1. A Reflected Cross-Site Scripting (XSS) Vulnerability
  2. Absence of CORS protection
  3. Storage of a Salesforce token in Local Storage
  4. Ability to fetch an Account ID on a specific endpoint with only the Salesforce token found in the previous step.
  5. Finding my targets to phish

Here’s how I did it, and more importantly how I was thinking to get to executing this exploit.

Reflected XSS on Analytics Endpoint

This all started out with the discovery of an old endpoint (found via a tool called waybackurls) that looked interesting as I don’t recall seeing it while using the app manually. I browsed to it and noticed that this endpoint allowed for reflected XSS via an analytics parameter (never skip those!). This query parameter was not properly sanitized therefore allowing me to execute Javascript in a victim’s context. Tip: Typically, researchers will report this with an alert box as a proof of concept and collect a Low/Medium bounty and move on. I recommend for you to stop doing this if you enjoy proving a larger impact to the company (and also being paid 5–10x more for your findings…).

Absence of CORS protection

At first, I tried to a javascript file from my VPS, but then my browser blocked it, viewed via the Network tab in the DevTools. Why? Because my simple HTTP server is not configured with SSL, therefore HTTP is only being used as opposed to HTTPS. The browser has a built-in security mechanism to block requests if they originate from a secure (HTTPS) site towards a non-secure (HTTP) one. Content Security Policy (CSP) is a mechanism that protects a website from importing resources from external sites — but only if used correctly. This site didn’t use it. This was a wonderful lesson learned about javascript and browser functions.

What did I do next? I hosted my code on an HTTPS endpoint and imported it that way. This javascript did something fairly benign but I quickly learned I could actually import it and run any javascript code I wanted, also as the same Origin as the website — the fun begins…

Salesforce Token in Local Storage

Now the question remains… How do I escalate this as much as possible to prove impact and augment the severity of this simple XSS bug? I started with the Account Profile section and saw what kinds of requests were changing my profile data. It turns out, all the requests were in the same format and were sending POST data to a common Salesforce Lightning endpoint on the app. It looked something like this:

POST /s/sfsites/aura?r=<int>&aura.ApexAction.execute=1&…

Apologies for the heavy redaction, it is required to not expose the company!

Requests all contained the same four parameters: message, aura.context, aura.pageURI and aura.token.

I then started tampering with these parameters one by one and seeing what I could delete, all while executing the original request. Turns out all the endpoints I hit used the same context and path — don’t need to touch those. The variables here were the message and the aura.token. I had to figure out what these were and how were they set. I began with the aura.token. At first, I saw that it simply a JSON object that was the same across all endpoints, but different with different accounts… that must be tied to me or my session somehow. I searched for it in the cookies, and couldn’t find it — so I then thought, how was this token getting into my requests? I then found it in the Local Storage of my browser.

Salesforce Token in Local Storage

With this knowledge, I could execute a script to log this token to the console:

var sf_token = localStorage.getItem('$AuraClientService.token$siteforce:communityApp');
console.log("Salesforce token is: " + sf_token);

Sure, I could send this to my external hosted server, but with only this I could not perform any sensitive actions because I was still missing one crucial element for making authenticated requests — the message value.

The easiest way to figure out the message parameter was to change my email in the account dashboard and observe the request it made. By comparing this request with a request from a second account I made, there was only one thing that changed : an Account ID. The next step was to figure out where this was pulled from, as it also was not in the cookies.

Hunting the Account ID

Because I am using Burp Pro, I have the ability to quickly “search” across my entire project and pinpoint where to locate this ID string in the responses (use the filters!). One response in particular reflected this in the response… and to fetch this, all that was required was the Salesforce token from the user. This is where the excitement kicked in for me as I already found a method to steal this.

I quickly scripted (thanks ChatGPT) a few lines of code to leverage my XSS bug to steal the Account ID by using the stolen Salesforce Token. From there, it was trivial to take these two pieces of information as variables, and use them in a request to change the victim’s email address to my own. I had all I needed to create an Account Takeover with this low finding Reflected XSS bug.

The final script had the following logic (“ChatGPT, make me a javascript file with the following logic…”)

  1. Capture the Salesforce token from Local Storage, store it in a variable.
  2. Use this variable along with the rest of the hardcoded (safe) request to call to the endpoint that reflected the Account ID, parsed the response and stored this ID in another variable.
  3. Use these two variables to change the state of the victim’s profile — i.e. change their email address to my own.

I could then go to this site, hit “Forgot my Password” and the reset token would drop right to my email for me to control a new password for their account. There was one flaw however, and something I could not surpass. In order to execute this request, I needed to provide a First Name, Last Name, Email and Phone Number (did not have to be valid), but this data would consequently overwrite their data upon script execution, meaning no easy PII for me when I logged in their account… I did however have access to anything else in their account, but out of respect for the company I will not divulge what this may be as to not give away the company name.

All I required was for them to click my link… but who’s them? This site is used by a niche set of people, and you’ve probably never heard of it. Spraying my phishing link will likely not do anything for me as for this to work, the victim requires to be logged in to this site… I need some valid users, some real targets.

Finding my victims

Months ago, I had originally reported a bug that allowed view access to all the registered users’ First Name and last name on the site, and reported as a Low because I truly believed that’s all it was. The triager had actually upgraded the severity to Medium (you rock Grace!)…

…but the company then marked this as Informative, refusing to pay (common trend with this company). The argument? These people can all be found online anyway. I countered with the argument that I too can be found online, and that’s beside the point. The point was that there was being personal data about registered users exposed to the world with no security preventing it. The company ignored that report from there on out. Although I do not agree with them (I have many valid usernames now that I could launch a password spray attack) I moved on… until now. Remember that I only required targets for this attack to be more successful? I have them. All of them. Granted, I only have their first and last names, but that’s enough to do some trivial OSINT (FName + LName + Medical|Doctor) to find their contact info and phish them, and ultimately delete them from this site.

If you’d like to learn more about testing Salesforce — read this: https://www.enumerated.ie/index/salesforce

Conclusion

Pretty cool security flaw and attack chain right? It would have paid $1,500 if triaged as a High (not a Critical because I still need a victim to click my link). I was paid……*drum roll*…... a Duplicate, aka sweet nothing. Out of curiosity, I asked the triager if the previous researcher escalated this to an ATO, and the answer was no — they reported the XSS and got paid for a (likely) low finding: $150. Because the initial vector was already reported (nearly two years ago!), my entire attack was now worth nothing.

I hope you enjoyed reading this, and as I improve my skills and find more ways to exploit and report vulnerabilities, I’ll be sure to share with all of you. It is largely through reading articles like this that I improve my skills in Bug Bounty Hunting so I feel it necessary to give back to the community as well.

Feel free to drop me a message on X (@swehtpantz) if you have any questions, or if something is not so clear — I would be more than happy to explain and help you succeed in your journey, as many are helping me in mine.

Until next time.

--

--

Swehtpantz
Swehtpantz

Written by Swehtpantz

Curiosity and persistence are the two most precious attributes of an attacker.

No responses yet