Tuần tự hoá (serialization) là biến một object trong bộ nhớ thành chuỗi byte để lưu hay gửi đi; giải tuần tự hoá (deserialization) là dựng lại object từ chuỗi đó. Nghe như một thao tác dữ liệu vô hại. Nhưng với một số định dạng — Python pickle, Java ObjectInputStream, PHP unserialize — việc "dựng lại object" có thể chạy mã. Nếu chuỗi byte đến từ kẻ tấn công, đó là thực thi mã từ xa. Bài này chứng minh RCE thật qua pickle và cho thấy vì sao JSON không có lỗ này.

Toàn bộ chạy trong container tự dựng, tự dọn. Chỉ tấn công tiến trình do tôi tạo. Không nhắm vào hệ thống của ai.

Giai tuan tu hoa khong an toan: pickle chay ma, JSON chi la du lieu

Cách đo

Python cho một object định nghĩa phương thức __reduce__ — trả về cách dựng lại chính nó khi được unpickle. Kẻ tấn công lợi dụng: cho __reduce__ trả về "gọi os.system với lệnh này". Khi ai đó pickle.loads dữ liệu đó, Python ngoan ngoãn gọi os.system — chạy lệnh của kẻ tấn công. Tôi dựng đúng payload đó và đo.

Số đo

Ba lần chạy, kết quả giống hệt:

Thao tác Kết quả
pickle.loads(payload độc) tạo /tmp/bangchung.txt chứa RCE-QUA-PICKLE
Payload chạy lệnh id uid=0(root) gid=0(root)
json.loads cùng dữ liệu trả về một dict — không chạy gì

Dòng đầu là RCE thật: chỉ tải dữ liệu là đã chạy lệnh — không có lời gọi hàm nào khác, không có bước "thực thi" riêng. pickle.loads() vừa giải mã vừa thực thi trong cùng một thao tác. Dòng hai cho thấy hậu quả: lệnh chạy với quyền của tiến trình — ở đây là root.

Điều đáng nhớ

Lỗ hổng nằm ở chỗ pickle lưu cách dựng lại object, không chỉ lưu giá trị. Đây là gốc rễ, và nó phân biệt định dạng nguy hiểm với định dạng an toàn. Khi pickle một object, nó ghi lại "gọi hàm nào, với tham số gì, để tái tạo object này". Thông tin đó cần thiết để dựng lại các object phức tạp — nhưng nó cũng có nghĩa là chuỗi byte chứa lời chỉ dẫn gọi hàm. Kẻ tấn công chỉ cần làm cho hàm được gọi là os.system thay vì một constructor vô hại. JSON thì ngược lại: nó chỉ lưu giá trị — số, chuỗi, danh sách, từ điển. Không có chỗ nào trong JSON để nói "gọi hàm này", nên không có gì để khai thác.

"Chỉ tải dữ liệu" nghe an toàn, và đó là cái bẫy. Lập trình viên hình dung deserialize như parse — một thao tác đọc thụ động. Với JSON đúng là vậy. Với pickle, "đọc" và "thực thi" là cùng một hành động. Nhiều lỗ RCE nghiêm trọng đến từ việc một lập trình viên dùng pickle (hoặc Java serialization) để nhận dữ liệu qua mạng vì nó "tiện" — pickle được bất kỳ object Python nào, không cần viết mã chuyển đổi — mà không nhận ra tiện lợi đó đi kèm một cửa thực thi mã mở toang.

Đây không phải chuyện riêng của Python. Cùng cơ chế tồn tại ở Java (ObjectInputStream.readObject — nguồn của vô số CVE, gồm cả các chuỗi gadget nổi tiếng), PHP (unserialize với "POP chain"), Ruby (Marshal.load), .NET (BinaryFormatter, đã bị Microsoft khuyến cáo bỏ). Mọi ngôn ngữ có một định dạng tuần tự hoá "giàu" — lưu được cả hành vi chứ không chỉ dữ liệu — đều mang lỗ này. Chi tiết khai thác khác nhau (Java cần một "chuỗi gadget" các lớp có sẵn), nhưng nguyên tắc chung: giải tuần tự hoá dữ liệu không tin cậy bằng định dạng giàu = giao cho kẻ tấn công một phần quyền điều khiển tiến trình.

Payload trông thế nào

Payload độc trong phép đo là kết quả của một class nhỏ:

class DocHai:
    def __reduce__(self):
        return (os.system, ("lenh cua ke tan cong",))

__reduce__ là hook Python dùng để pickle biết cách dựng lại object. Ở đây nó nói dối: thay vì trả về cách tái tạo một DocHai, nó trả về "gọi os.system với lệnh này". Khi pickle.loads gặp chỉ dẫn đó, nó gọi đúng như được bảo. Chuỗi byte pickle bắt đầu bằng €... (base64: gASV...) mã hoá chính lời gọi đó — kẻ tấn công chỉ cần gửi chuỗi này tới bất cứ nơi nào máy chủ sẽ pickle.loads.

Điểm đáng sợ: object DocHai không cần tồn tại ở phía máy chủ. pickle mang theo tên module và hàm cần gọi (os, system) — những thứ có sẵn ở mọi tiến trình Python. Kẻ tấn công không cần biết gì về mã của bạn, chỉ cần biết bạn pickle.loads dữ liệu của nó.

Vì sao

Sự khác biệt giữa pickle và JSON là sự khác biệt giữa dữ liệuchương trình:

  • JSON là một ngôn ngữ dữ liệu: ngữ pháp của nó chỉ có object, mảng, chuỗi, số, boolean, null. Không có động từ, không có lời gọi. Đọc JSON không thể làm gì ngoài tạo ra các giá trị đó.
  • pickle là một máy ảo nhỏ: định dạng của nó có các lệnh, gồm "gọi đối tượng có thể gọi này với các tham số kia". Đọc pickle là chạy các lệnh đó. Nó gần với "thực thi một chương trình" hơn là "phân tích một tài liệu".

Đó là lý do lời khuyên không phải "dùng pickle cẩn thận" mà là "đừng dùng pickle cho dữ liệu không tin cậy" — cái nguy hiểm là bản chất của định dạng, không phải cách dùng.

Nghĩa là gì trong thực tế

  • Dùng JSON (hoặc định dạng chỉ-dữ-liệu khác) cho mọi dữ liệu qua mạng hoặc từ người dùng. JSON, Protocol Buffers, MessagePack — chúng biểu diễn dữ liệu mà không mang hành vi.
  • Đừng bao giờ pickle.loads / readObject / unserialize dữ liệu bạn không tự sinh và bảo vệ. Kể cả từ cache, từ hàng đợi, từ cookie — nếu kẻ tấn công chạm được vào chuỗi byte, đó là RCE.
  • Nếu bắt buộc dùng định dạng giàu (ví dụ giữa các dịch vụ nội bộ tin nhau), ký HMAC chuỗi byte và kiểm chữ ký trước khi giải — để chỉ dữ liệu do bạn sinh mới được giải.
  • Vô hiệu hoá các bộ giải tuần tự hoá nguy hiểm mặc định. .NET đã bỏ BinaryFormatter; Java có bộ lọc lớp (ObjectInputFilter) để giới hạn lớp nào được phép giải.

Chỗ tôi không kết luận được

Tên phần này nói "hai ngôn ngữ", nhưng tôi chỉ dựng RCE thật trên Python pickle — nơi payload chỉ vài dòng. Java và PHP mang cùng lỗ hổng nguyên tắc, nhưng khai thác thực tế cần một "chuỗi gadget" — ghép các lớp có sẵn trong classpath thành một chuỗi dẫn tới thực thi — mà việc dựng lại cần đúng phiên bản thư viện và nhiều mã hơn một bài đo cho phép. Tôi chứng minh cơ chế trên ngôn ngữ dễ nhất và mô tả sự tương đương, thay vì dựng ba RCE nửa vời.

Và payload của tôi chạy với root vì container chạy root mặc định — trên hệ thật, tiến trình web thường chạy quyền thấp hơn, nên hậu quả tuỳ thuộc quyền của tiến trình. Nhưng "quyền thấp hơn" không phải phòng thủ: một RCE quyền thấp vẫn là RCE.

Thử ba mươi giây

Tìm trong mã của bạn các lời gọi pickle.loads, pickle.load (Python), readObject (Java), unserialize (PHP), Marshal.load (Ruby), BinaryFormatter (.NET).

Với mỗi chỗ, truy ngược: dữ liệu đầu vào đến từ đâu? Nếu có bất kỳ đường nào để dữ liệu đó chạm tới người dùng — một trường form, một cookie, một tin nhắn hàng đợi, một tệp tải lên — bạn có một lỗ thực thi mã từ xa. Chuyển sang JSON, hoặc ký HMAC nếu bắt buộc giữ định dạng giàu.