+2

Prototype Pollution attack (phần 1)

I. Kiến thức chuẩn bị

Mở đầu chuỗi bài viết về chủ đề Prototype Pollution, chúng ta sẽ cùng làm quen với các khái niệm Object, Prototype. Phạm vi tìm hiểu ban đầu của chúng sẽ nằm trong ngôn ngữ JavaScript - loại ngôn ngữ lập trình tiêu biểu và đặc trưng dựa trên mô hình prototypal inheritance (prototypal inheritance model) để quản lý đối tượng.

1. Object trong JavaScript

Trong JavaScript, đối tượng (object) là một thực thể bao gồm thuộc tính (properties) và phương thức (methods) liên quan đến nó, cấu tạo có dạng key:value.

var person = {
    name: "John",
    age: 25,
    introduce: function () {
        console.log("Xin chào, tôi là " + this.name + " và tôi " + this.age + " tuổi.");
    }
};

Trong ví dụ trên, đối tượng person có các thuộc tính nameage, phương thức introduce. Đối tượng này được khởi tạo theo cú pháp object literal. Ngoài ra có thể sử dụng constructor function:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.introduce = function () {
        console.log("Xin chào, tôi là " + this.name + " và tôi " + this.age + " tuổi.");
    };
}

var person = new Person("John", 25);

Để truy cập thuộc tính và phương thức của đối tượng, ta sử dụng cú pháp dấu chấm (dot notation) hoặc dấu ngoặc (bracket notation). Ví dụ:

console.log(person.name); // Kết quả: "John"
person.introduce(); // Kết quả: "Xin chào, tôi là John và tôi 25 tuổi."

2. Prototype trong JavaScript

Trong cách khởi tạo constructor function phía trên, chú ý rằng mỗi class (lớp) trong JavaScript được thể hiện dưới dạng function (hàm):

function Foo() {
    this.bar = 1;
}

foo = new Foo();

Một object có thể được khởi tạo dựa trên class này thông qua từ khóa new. Bởi vậy có thể coi class là một dạng template (bản mẫu) của object. Tương tự, class cũng có template của nó, chính là prototype (nguyên mẫu). Có thể truy cập qua hai cách:

  • Gọi từ class: Foo.prototype

image.png

  • Gọi từ object: foo.__proto__

image.png

Một số ví dụ khác khi khởi tạo đối tượng thuộc các lớp đặc biệt Object, String, Array, Number:

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

Đối tượng trong JavaScript cũng có thể kế thừa từ các prototype, cho phép tái sử dụng mã nguồn và chia sẻ các thuộc tính và phương thức giữa các đối tượng khác nhau.

function Person(name) {
  this.name = name;
}

Person.prototype.introduce = function() {
  console.log("Xin chào, tôi là " + this.name + ".");
};

var person1 = new Person("John");
person1.introduce(); // Kết quả: "Xin chào, tôi là John."

Trong ví dụ này định nghĩa một constructor function Person và thêm phương thức introduce vào prototype của nó. Đối tượng mới person1 được tạo từ Person sẽ kế thừa từ prototype và có thể sử dụng phương thức introduce.

3. Mô hình Prototypal inheritance (Kế thừa prototypal)

Mô hình prototypal inheritance trong JavaScript là một cách thức tạo ra sự kế thừa giữa các đối tượng thông qua prototype. Trong mô hình này, mỗi đối tượng có một prototype, và các đối tượng mới có thể kế thừa các thuộc tính và phương thức từ prototype của chúng. Cùng xem ví dụ sau:

// Định nghĩa một nguyên mẫu (prototype)
var personPrototype = {
    introduce: function() {
        console.log("Xin chào, tôi là " + this.name + " và tôi " + this.age + " tuổi.");
  }
};

// Tạo một đối tượng mới kế thừa từ prototype
var person1 = Object.create(personPrototype);
person1.name = "John";
person1.age = 25;

// Gọi phương thức introduce trên đối tượng
person1.introduce();

Chương trình tạo một prototype personPrototype bằng cách khai báo một đối tượng với phương thức introduce. Sau đó, đối tượng mới person1 được tạo bằng phương thức Object.create() và truyền vào personPrototype làm prototype. Đối tượng mới này sẽ kế thừa tất cả các thuộc tính và phương thức từ personPrototype.

Prototype-based inheritance là một đặc điểm quan trọng của JavaScript, giúp tạo ra các mối quan hệ đối tượng linh hoạt và tiết kiệm bộ nhớ.

II. Lỗ hổng Prototype Pollution

1. Nguyên lý

Dựa theo cách hoạt động của mô hình prototypal inheritance, khi chúng ta gọi một thuộc tính từ đối tượng, JavaScript sẽ hiển thị giá trị bằng cách truy xuất thuộc tính đó "từ thấp lên cao". Chẳng hạn, khi được yêu cầu hiển thị foo.temp, nếu giá trị này không tồn tại sẽ tìm tới foo.__proto__.temp, không tồn tại sẽ tiếp tục tìm tới foo.__proto__.__proto__.temp ... cho tới khi gặp giá trị null mới dừng lại.

foo.temp; // undefined
foo.__proto__.temp; // undefined
foo.__proto__.__proto__.temp; // undefined
...
// null

Từ đây dễ dàng nhận thấy rằng nếu chúng ta có thể thay đổi giá trị các thuộc tính trong foo.__proto__, thì các thuộc tính của foo cũng sẽ thay đổi theo.

image.png

Đồng thời, việc thay đổi thuộc tính của class Foo() không ảnh hưởng đến thuộc tính trong đối tượng foo (Do cách hoạt động của prototypal inheritance).

image.png

Sự thay đổi như trên chỉ ảnh hưởng tới các đối tượng thuộc class Foo(). Xa hơn nữa, để gây ảnh hưởng tới thuộc tính của tất cả đối tượng nói chung, chúng ta sẽ cần "can thiệp" vào prototype của foo.__proto__, thật vậy:

image.png

Đây cũng chính là nguyên lý hoạt động của lỗ hổng Prototype Pollution: Sự can thiệp vào các thuộc tính nằm trong prototype của class Object nói chung (foo.__proto__.__proto__) dẫn đến sự "pollution" tất cả thuộc tính của các đối tượng mới được tạo ra.

2. Ví dụ

Để hiểu rõ hơn về dạng lỗ hổng này, chúng ta cùng xem xét ví dụ sau

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

Hàm merge() cho phép hợp nhất các phần tử từ source vào target, cụ thể: nếu source chứa phần tử x trong khi target chưa có x, hàm sẽ sao chép phần tử x từ source vào target.

image.png

Một câu hỏi đặt ra tại dòng code target[key] = source[key]: Nếu key nhận giá trị là __proto__, ví dụ với object2 = {a: 1, "__proto__": {b: 2}};, thì chúng ta có thể thực hiện prototype pollution hay không? Rất tiếc rằng câu trả lời là chưa thể thực hiện được:

image.png

Khá bất ngờ phải không nhỉ! Thực ra ngay sau khi đối tượng object2 được tạo ra, thì __proto__ lúc này đã đóng vai trò là prototype của object2 rồi, không còn đóng vai trò là key nữa. Dễ ràng kiểm chứng bằng cách liệt kê các key của object2:

image.png

Tức là, đối với các trường hợp __proto__ đóng vai trò là một key thì lỗ hổng tồn tại. Chẳng hạn với hàm JSON.parse():

JSON.parse('{"a": "1", "__proto__": {"b": "2"}}')

image.png

Lúc này, tất cả đối tượng mới được tạo ra đều bị ảnh hưởng:

image.png

Một đoạn code khác chứa lỗ hổng:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}
let o1 = JSON.parse('{"constructor": {"prototype": {"hello": 1}}}')
merge({},o1)

let o2 = {}
console.log(o2.hello)

Từ các phân tích phía trên, chúng ta có thể tổng hợp lại các điều kiện cần đồng thời thỏa mãn trong một cuộc tấn công Prototype Pollution:

  • Đối tượng được thực hiện hợp nhất đệ quy hoặc các hành vi tương tự.
  • Thuộc tính đối tượng được xác thực theo phương thức kế thừa prototypal.

Ở phần tiếp theo chúng ta sẽ tìm hiểu và phân tích một số kỹ thuật tấn công, khai thác lỗ hổng Prototype Pollution.

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


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


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.