+2

Prototype Pollution attack (phần 4)

IV. Phân tích và khai thác các kỹ thuật phát hiện, tấn công Server-side - Prototype Pollution (tiếp)

5. Sử dụng các phương thức ghi đè kiểm tra lỗ hổng

5.1. Ghi đè Status code

Trong quá trình sử dụng trang web, đôi khi chúng ta gặp các tình trạng không thể truy cập một endpoint nào đó, điều này xảy ra có thể do nhiều nguyên nhân. Thường thấy nhất là do chúng ta không đủ quyền hạn để truy cập. Trong BurpSuite, response khi truy cập endpoint đó có thể trả về trạng thái lỗi trong kiểu JSON như sau:

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

Chương trình tạo ra response như trên có thể bị khai thác bởi lỗ hổng Prototype Pollution, chẳng hạn hàm createError() trong module http-errors của Nodejs, tham khảo tại index.js trong https://www.npmjs.com/package/http-errors?activeTab=code:

function createError () {
    //...
    if (type === 'object' && arg instanceof Error) {
        err = arg
        status = err.status || err.statusCode || status
    } else if (type === 'number' && i === 0) {
    //...

Đoạn code status = err.status || err.statusCode || status kiểm tra giá trị err.status hoặc err.statusCode nếu tồn tại sẽ gán giá trị cho status, ngược lại status sẽ giữ nguyên giá trị của nó. Ý tưởng tự nhiên là có thể thay đổi thuộc tính status trong Object.prototype, từ đó thay đổi được giá trị status theo ý muốn. Tuy nhiên, cần lựa chọn giá trị phù hợp cho Object.prototype.status vì:

if (typeof status !== 'number' || (!statuses.message[status] && (status > 400 || status >= 600))) {
    status = 500
}
//...

Đoạn code kiểm tra nếu status mang giá trị nằm ngoài đoạn [400;599][400;599] sẽ thay đổi về giá trị mặc định là 500500, bởi vậy nên lựa chọn giá trị ghi đè trong khoảng này.

5.2. Ghi đè Charset

Charset (Bảng mã) được sử dụng để xác định cách mã hóa các ký tự. Khi một trang web được tải, thông tin charset thường được xác định trong thẻ meta để trình duyệt biết cách hiển thị các ký tự đúng định dạng:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Trang web ví dụ</title>
</head>
<body>
  <!-- Nội dung trang web -->
</body>
</html>

Đối với Express, middleware module body-parser cũng cung cấp một tùy chọn cho phép cài đặt định dạng ký tự hiển thị là defaultCharset, theo https://expressjs.com/en/resources/middleware/body-parser.html:

Specify the default character set for the text content if the charset is not specified in the Content-Type header of the request. Defaults to utf-8.

Điều đó được thể hiện rõ hơn trong đoạn code sau (đoạn code có thể tìm thấy tại https://github.com/expressjs/body-parser/blob/ee91374eae1555af679550b1d2fb5697d9924109/lib/types/json.js):

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
})

Bắt đầu với hàm read(), các gói tin được truyền vào để xử lý, trong đó biến endcoding được xác định qua lệnh gọi hàm getCharset() hoặc mặc định với giá trị UTF-8. Với hàm getCharset() cho phép xác định định dạng ký tự thông qua tham số charset trong header Content-type. Như vậy, nếu contentType.parse(req).parameters.charset bị thay đổi sẽ ảnh hưởng tới quá trình giải mã của dữ liệu.

Về phương thức kiểm tra, có thể sử dụng định dạng UTF-7. Chẳng hạn, mã hóa của foo trong định dạng UTF-7+AGYAbwBv-. Gửi request với dữ liệu như sau:

{
    "username":"viblo",
    "role":"+AGYAbwBv-"
}

Lúc này, do hệ thống sử dụng định dạng giải mã UTF-8 nên chuỗi +AGYAbwBv- không bị thay đổi. Thực hiện lây nhiễm thuộc tính content-type:

{
    "username":"viblo",
    "role":"default",
    "__proto__":{
        "content-type": "application/json; charset=utf-7"
    }
}

Trong trường hợp ứng dụng tồn tại lỗ hổng Prototype Pollution, kết quả response sẽ giải mã theo định dạng UTF-7 chuỗi +AGYAbwBv- và trả về chuỗi foo.

Phương thức kiểm tra lỗ hổng này bạn đọc có thể thử sức trong bài lab Detecting server-side prototype pollution without polluted property reflection.

6. Phát hiện lỗ hổng tự động với công cụ

Bên cạnh việc kiểm tra sự tồn tại của lỗ hổng Prototype Pollution server-side với các phương pháp đã nêu trên, chúng ta còn có thể sử dụng một số công cụ tự động hóa các quy trình nhằm tiết kiệm thời gian và công sức, đặc biệt trong bối cảnh kiểm thử ứng dụng có khối lượng công việc lớn.

Một trong những công cụ mang lại hiệu quả tốt với dạng lỗ hổng này là extension Server-Side Prototype Pollution Scanner của BurpSuite.

image.png

Chúng ta có thể kiểm tra từng request riêng biệt hoặc một nhóm các requests được bắt qua BurpSuite. Ví dụ kiểm tra tính năng update tại endpoint /my-account/change-address. Tại phần request > Click chọn chuột phải > Extensions > Server-Side Prototype Pollution Scanner > Server-Side Prototype Pollution

image.png

Tại đây cho phép lựa chọn nhiều kỹ thuật rà quét khác nhau, hầu như bao gồm đầy đủ tất cả các kỹ thuật đã được nhắc tới trong các phần trên. Bạn đọc cũng có thể chọn Full scan để kiểm tra tổng quát toàn bộ kỹ thuật detection. Kết quả phỏng đoán hiển thị tại Issue activity:

image.png

7. Từ lỗ hổng Prototype Pollution dẫn đến Remote Code Execution (RCE)

Khi một ứng dụng web cho phép sử dụng nguồn dữ liệu không tin cậy để cập nhật các đối tượng JavaScript, kẻ tấn công có thể chèn các giá trị độc hại trong các dữ liệu này nhằm tấn công lỗ hổng Prototype Pollution. Đối với dạng lỗ hổng ảnh hưởng trực tiếp tới các thuộc tính trong hệ thống và chương trình vĩnh viễn, ở một số trường hợp cụ thể có thể dẫn đến các hậu quả nghiêm trọng, bao gồm Remote Code Execution (RCE) - Thực thi mã tấn công từ xa. Trong phần này, chúng ta sẽ cùng tìm hiểu một số case study cụ thể trong ngôn ngữ NodeJS.

Một số phương thức có nguy cơ bị khai thác có thể kể đến như child_process.fork(), child_process.spawn()child_process.execSync(). Với child_process.fork(), theo tài liệu từ Nodejs:

child_process.fork(): spawns a new Node.js process and invokes a specified module with an IPC communication channel established that allows sending messages between parent and child.

Hiểu một cách đơn giản, phương thức child_process.fork() cho phép tạo một tiến trình con độc lập để xử lý các công việc. Tiếp tục để ý cấu trúc cú pháp của phương thức này:

child_process.fork(modulePath[, args][, options])

Đặc biệt với tùy chọn options chứa tham số execArgv, theo https://github.com/nodejs/node/blob/main/doc/api/child_process.md#child_processforkmodulepath-args-options:

execArgv {string[]} List of string arguments passed to the executable. Default: process.execArgv

execArgv là một danh sách các arguments (đối số) được truyền vào phương thức để thực thi, và có giá trị mặc định process.execArgv.

Do đó, trong trường ứng dụng bị ảnh hưởng bởi lỗ hổng Prototype Pollution, chúng ta có thể thay đổi giá trị thuộc tính execArgv để nâng cấp lỗ hổng dẫn đến việc RCE thông qua phương thức này. Chẳng hạn, bằng cách sử dụng argument --eval để đưa các dòng lệnh vào tiến trình con thực thi. Để thực hiện các lệnh, chúng ta sẽ sử dụng phương thức execSync() trong module child_process.

"__proto__": {
    "execArgv":[
        "--eval=require('child_process').execSync('lệnh thực thi')"
    ]
}

Cụ thể, chúng ta xem xét chương trình ví dụ sau:

const { execSync, fork } = require('child_process');

function isObject(obj) {
    console.log(typeof obj);
    return typeof obj === 'function' || typeof obj === 'object';
}

// Function vulnerable to prototype pollution
function merge(target, source) {
    for (let key in source) {
        if (isObject(target[key]) && isObject(source[key])) {
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

function clone(target) {
    return merge({}, target);
}

// Run prototype pollution with user input
// Check in the next sections what payload put here to execute arbitrary code
clone(USERINPUT);

// Spawn process, this will call the gadget that poputales env variables
// Create an a_file.js file in the current dir: `echo a=2 > a_file.js`
var proc = fork('a_file.js');

Với hàm merge() quen thuộc cho thấy chương trình tồn tại lỗ hổng Prototype Pollution. Ngoài ra, chương trình sử dụng hàm fork() khởi tạo một chương trình con, gọi file a_file.js và thể thực hiện các tác vụ được đề cập trong file này.

var proc = fork('a_file.js');

Đây cũng là điểm kích hoạt tấn công RCE dựa trên kỹ thuật lợi dụng phương thức child_process.fork(), vì trước đó thuộc tính execArgv trong argument của phương thức có thể đã bị kẻ tấn công thay đổi dựa vào lỗ hổng Prototype Pollution.

Với phương thức child_process.spawn() cũng có bản chất tương tự, xin dành cho bạn đọc tìm hiểu tại link.

Bạn đọc có thể luyện tập kỹ thuật tấn công này với bài lab Remote code execution via server-side prototype pollutionExfiltrating sensitive data via server-side prototype pollution

Các tài liệu tham khảo


©️ Tác giả: Lê Ngọc Hoa từ Viblo


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í