WSA: Reflected DOM XSS
Table of Contents
Introduction
This was the most interesting XSS lab I think I have seen so far. Not
only did it made me dig a little bit deeper than the others, which
could have been due to overlooking eval()
, but it also had a fun part
of thinking out of the box: developing a payload, no matter how little
time it takes, is always satisfying in the end!
Challenge
You can check the lab out: here
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.
Search functionality
First things first, checking out t
<script src="/resources/js/searchResults.js"></script> <script>search('search-results')</script>
Analyzing the vulnerable JS snippet
Plainly: looking at 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); } }
The script, once search(path)
is called evaluates the response that
came with our search request, as shown in the following picture:
Figure 1: Search and Response
Then the function displaySearchResults()
is called, which taking the
information from searchResultsObj
(what was returned), populates the
results page.
The searchTerm
(our input) gets added in h1.innerText
, while for every
searchResult
the following process is followed:
- If there is an
id
field:- Creates a ink element pointing to
"/post?postId=" + searchResult.id
- If there is a
headerImage
- Adds an
img
with source:"/image/" + searchResult.headerImage
- Adds an
- Creates a ink element pointing to
- If there is a
title
field:- Creates a
<h2>
element withinnerHTML
- Creates a
- If there is a
summary
field- Creates a
<p>
element withinnerHTML
- Creates a
- If there is a
id
field- Creates a
<a>
, “View Post” element: with a link similar to that of the first if
- Creates a
Finally, some non-directly manipulatable actions insert Back to Blog
link.
Keep Going
Not going to lie, I felt stuck:
innerHTML
was tricky in the last lab as well and this time it was made even trickier.- Angle brackets must be encoded: I had avoided that by setting
parameters, but now, writing inside of an
innerHTML
property, that is not applicable - Taking a hint: the
eval()
function when handling input
Back
This is the vulnerable snippet:
if (this.readyState == 4 && this.status == 200) { eval('var searchResultsObj = ' + this.responseText); displaySearchResults(searchResultsObj); }
And our search query is included in that. After playing around in repeater, I developed a payload that gave me promising results:
Figure 2: Promising Payload
Using that very same payload in the search field and the lab is marked as solved
Summary
DO NEVER OVERLOOK EVAL AGAIN, I KNEW BETTER THAN THAT