From CRLF to Account Takeover

Many people don’t like client-side vulnerabilities. I’m not a fan of such vulnerabilities as well. And I try to spend less time searching for them. You can’t surprise anyone with endless alert-boxes on the pages. But sometimes these alerts boxes can be worth their weight in gold. Especially if the execution of javascript is necessary for the chain to exploit a serious problem. Under a serious problem today we are talking about stealing user account.

In a classic XSS attack scenario, there is always reading user data, getting a token from local storage or cookies, modifying user data, changing data to steal an account. Typically, the hijacking is carried out through a change of email or password. To protect against that classic attack scenario came CSRF tokens. But these methods of protection can sometimes be bypassed by passing an empty token value, or do not pass at all csrf token in the request, or pass token but from another user session. There is a lot of tricks to bypass that protection layer and perfectly explained from 0ang3el here and here. But the advent of a global browser policy is not far off and will kill classic XSS+CSRF attacks with SameSite by default. Now we still have time to use the CSRF features because the release of changes in browsers has been slightly shifted due to the situation in the world.

At the beginning of March, while researching one site I discovered the new functionality. The functionality allowed the user to login via SMS. It’s convenient in a situation when you couldn’t remember the password. I immediately went to the user’s profile and watched the appearance of new fields with the phone number in the account. But I didn’t find anything new as a result. But I realized that the phone number is loaded from the section where the user can specify the number to receive delivery notifications. So, the developers simply took and started using the existing functionality without any additional security measures and validation. And there really was no validation. It was possible to just change the phone without any confirmation. The only validation that was there was the check that the phone is not by other users. In my head, I immediately had an attack scenario to change user number via XSS and Taking Over account via login by SMS. For this plan, I had everything in my head. I had already imagined how I would find some XSS on my subdomain and implement an attack scenario. Yes, the XSS from the subdomain could use the current user session because the CORS policy allowed all subdomains to trust the main domain.

My puzzle was a chain of XSS -> Misconfigured CORS -> Account takeover.

But I was disappointed with the result. There was no XSS and I left my idea for an attack until better times. A month later, I came back and tried again. But again nothing. A couple of months later, I decided to check all the company’s subdomains on this site, including the range of network addresses for possible misconfigurations. I was a little surprised by the result. I found a classic way to do CRLF via %0D%0Amyheader:mydata%0D%0A. Next, I checked which addresses I could find domains and subdomains related to the organization with securitytrails. In total it was about 10 valid CRLFs on the entire range.

And then I remembered that very often you can make XSS from CRLF through a vector like this (from here).

Decoded view:

Then I watched blackfan report. And I tried that kind of thing.

And I did it. I mean, I thought I did it… I got a reflection with an alert box. Also, I was lucky enough to find a subdomain based on the vulnerable domain name. Don’t forget that I was testing on Firefox+Burpsuit bundle and I was already happy with the result. But then I was very surprised that my attack vector does not work if I try to reproduce the problem without Buprsuit. It didn’t work at all.

The browser humbly showed me a bug in the console — net::ERR_INVALID_CHUNKED_ENCODING 404 (Not found)

I didn’t have anything enabled in Burp. I just proxied all data without changes or modifications. However, it wasn’t quite so. At first, it turned out that when I was doing HTTP Response Splitting, I sort of jumped into the chunk part. At the same time, I was violating their consistency. BurpSuit behaves like a gentleman and decides to parse “broken” chunks, delivering me the entire response from the server as a complete one. As a result, Firefox under Burpsuit returns me a normal page with my XSS where I moved it to the body of the response.

But browsers without Burpsuit or curl return “emptiness” and disappointment.

You can read more about this hidden configuration in Burpsuit here — Strip chunked encoding metadata in streaming responses
"Strip chunked encoding metadata in streaming — Streaming is generally chunked-encoded over HTTP. If this option is selected, Burp will remove the chunked encoding metadata, making the responses more easily readable within Burp. Note that removing this metadata may break the client-side application, depending on how it is implemented"

I have been using BurpSuit for a long time. But the presence of this setting surprised me a lot. Big thanks to blackfan for the advice with chunk encoding. After overcoming the technical gap in this area, I managed to add a correctly generated GET request with my Reflected XSS. It was important to correctly calculate the size of the chunk. Specify this size from DEC to HEX. In the end, for my CRLF it was important to put the sign of ending chunk = 0. This allowed me to fully perform adding my XSS to the response body and discarding the rest of the response.

Decoded view:

But the browser didn’t like something again. There was a new mistake waiting for me in the console — net::ERR_CONTENT_DECODING_FAILED 404 (Not found)

But adding Content-Encoding: utf-8 solved the problem.
As a result, the final reflected XSS vector was this one.


I was very pleased with the result. It was only necessary to implement XHR POST request for getting CSRF token and making POST request for changing phone number in the victim account.

The body of my request looked like this.

Where the unique parameters were customerID and csrf_token.

Through a lot of errors, it was possible to implement obtaining of these parameters in the following way

The output of parameters is made in the console to demonstrate and test the idea itself.

The final script to change the victim’s phone number looked like this.

Adding customerID should have been done simply by inserting a number from the victim’s page. For this reason, it looks as follows in the code — “ + customer_id + “
For csrf_token parameter the insertion was made in quotation marks on the same principle — \”” + token + “\”.
This was the key moment for me. Because in this place I made a mistake and did the insertion of the first parameter in quotes. And my whole chain of vulnerabilities didn’t work properly.

The attacker’s final code could look like this.

By opening this link, the victim receiving XHR code in his browser at and then executing it. The result is that the victim's phone number in the account changed to the attacker’s phone number. And the attacker could login with his phone number to the victim account. As a result, after chaining several problems, I was able to perform a full-fledged attack, which is considered critical in the bug bounty program. Thanks to all of my friends who contributed to overcoming the problems on the way to the full attack vector.

PS: Click 👏 “Clapping Hands” icon if you like this article 😉


I am a guy passionate about testing and security researching 👨‍💻 →

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store