Phân tích lỗ hổng Insecure Deserialization (Object Injection) [p1]
Some personal note for security learning
I. Bản chất và nguyên nhân cốt lõi
Cơ chế hoạt động bình thường của chức năng
Trong lập trình, serialization (tuần tự hóa) là quá trình chuyển đổi một đối tượng (cùng trạng thái các thuộc tính của nó) thành chuỗi byte hoặc chuỗi ký tự để lưu trữ hoặc truyền đi.
![Quá trình Serialize và Deserialize [src: portswigger]](https://images.spiderum.com/sp-images/cb847fb0a19611f0af7ec5e21b00b771.png)
Quá trình Serialize và Deserialize [src: portswigger]
Ví dụ, ứng dụng web có thể tuần tự hóa một object người dùng và lưu vào cookie hoặc cơ sở dữ liệu. Khi cần sử dụng lại, ứng dụng sẽ deserialization (giải tuần tự) chuỗi dữ liệu đó để khôi phục thành đối tượng ban đầu.
![Quá trình Serialize và Deserialize qua web [src: portswigger]](https://images.spiderum.com/sp-images/c8169e90a19511f0a28fddc559aa9f19.png)
Quá trình Serialize và Deserialize qua web [src: portswigger]
Một request hợp lệ thường hoạt động như sau: phía client gửi chuỗi dữ liệu đã được tuần tự hóa (ví dụ: cookie phiên chứa đối tượng người dùng đã mã hóa Base64). Server nhận request, giải mã (như Base64 decode) rồi giải tuần tự chuỗi byte thành object tương ứng. Object này sau đó được hệ thống sử dụng trong logic xử lý – ví dụ, lấy thuộc tính
$user->name để hiển thị tên người dùng, hoặc kiểm tra quyền hạn qua $user->isAdmin. Dữ liệu đi qua các điểm: đầu vào (chuỗi tuần tự hóa từ người dùng) → xử lý (hàm deserialization tạo object) → đầu ra (sử dụng đối tượng trong logic bình thường, trả kết quả cho người dùng).Bản chất của lỗ hổng
Lỗ hổng insecure deserialization chủ yếu xuất phát từ lỗi của lập trình viên khi tin tưởng dữ liệu tuần tự hóa từ bên ngoài. Thay vì được xem như input nguy hiểm cần kiểm soát, dữ liệu này lại được giải tuần tự trực tiếp. Nói cách khác, ứng dụng cho phép deserialization trên dữ liệu do người dùng kiểm soát – đây chính là định nghĩa của lỗ hổng này. Vấn đề không nằm ở cấu hình hệ thống, mà ở chỗ lập trình viên không thực hiện kiểm tra an toàn trước khi giải tuần tự dữ liệu.
Thông thường, người lập trình không xác thực hoặc kiểm duyệt dữ liệu đầu vào trước khi unserialize. Họ có thể thiếu hiểu biết về mức độ nguy hiểm của việc deserialization dữ liệu không tin cậy. Một số người cố gắng kiểm tra tính hợp lệ sau khi đã deserialization (ví dụ: kiểm tra xem object có thuộc lớp mong đợi không), nhưng lúc này có thể đã muộn – payload độc hại có thể đã kích hoạt trong quá trình giải tuần tự. Ngoài ra, do định dạng tuần tự hóa thường là nhị phân hoặc khó đọc, lập trình viên ảo tưởng rằng kẻ tấn công không thể dễ dàng chỉnh sửa được dữ liệu này. Trên thực tế, dù định dạng phức tạp (nhị phân hay JSON, XML), kẻ tấn công vẫn có thể phân tích và tạo chuỗi tuần tự hóa tùy ý để tấn công.
Một điểm cốt lõi nữa là trong quá trình deserialization, các ngôn ngữ cho phép thực thi code một cách tự động thông qua các phương thức đặc biệt. Ví dụ, PHP sẽ tự động gọi phương thức
__wakeup() của đối tượng khi unserialize, Java cho phép định nghĩa method readObject() được chạy khi đọc đối tượng. Nếu những “magic method” này chứa logic nguy hiểm (như gọi hệ thống, xóa file, thực thi lệnh), chúng sẽ được kích hoạt mà không cần lập trình viên gọi tường minh. Đây không phải lỗi của ngôn ngữ, mà là do lập trình viên vô tình tạo điều kiện cho code độc hại chạy bằng cách unserialize dữ liệu không đáng tin cậy. Do đó, insecure deserialization thường được gọi là lỗ hổng Object Injection – kẻ tấn công có thể chích vào hệ thống một object tùy ý và lợi dụng các phương thức của nó.Tóm lại, đây là lỗi bảo mật technical do tin tưởng dữ liệu đầu vào. Ứng dụng thiếu cơ chế xác minh tính toàn vẹn (như chữ ký số) hoặc hạn chế lớp đối tượng được phép giải tuần tự. Hệ quả là đối tượng do kẻ xấu cung cấp sẽ được tạo ra trong ứng dụng và có thể thực thi các hành động không lường trước.
Điều kiện để xảy ra lỗi
Để lỗ hổng xảy ra, ứng dụng cần thỏa mãn một số điều kiện sau:
- Có sử dụng deserialization trên dữ liệu đầu vào: Tức là trong mã nguồn có chỗ gọi đến các hàm như unserialize() (PHP), Marshal.load/YAML.load (Ruby), ObjectInputStream.readObject() (Java) v.v. trên dữ liệu đến từ client. Nếu ứng dụng không hề deserialize dữ liệu từ người dùng thì không bị ảnh hưởng. Đây là điều kiện tiên quyết.
- Dữ liệu tuần tự hóa phải do người dùng kiểm soát: Ví dụ, chuỗi serialized được lưu ở cookie, tham số HTTP, trường ẩn form, tệp upload… mà kẻ tấn công có thể chỉnh sửa hoặc tạo mới. Nếu dữ liệu được ký số hoặc mã hóa để ngăn chỉnh sửa, kẻ tấn công phải có cách bypass (như biết được khóa ký). Nếu không, họ không thể cung cấp payload độc hại. (Trong phần sau chúng ta sẽ thấy cách kẻ tấn công lách cơ chế này nếu có).
- Môi trường có sẵn các lớp đối tượng/gadget phù hợp: Kẻ tấn công có thể tạo object của bất kỳ class nào có sẵn trong ứng dụng – quá trình deserialization thường không hạn chế loại đối tượng nạp vào. Điều này có nghĩa nếu ứng dụng hoặc thư viện dùng kèm có class dễ khai thác (ví dụ class có
__wakeup() thực thi lệnh hệ thống), thì khả năng RCE rất cao. Ngay cả khi không có class thực thi lệnh trực tiếp, kẻ tấn công vẫn có thể chuỗi nhiều class (gadget chain) để đạt được hành vi nguy hiểm cuối cùng. Nếu ứng dụng quá đơn giản, không có gadget hữu ích, có thể lỗ hổng chỉ dừng ở mức chỉnh sửa logic (như đổi quyền hạn người dùng) hoặc gây crash (DoS).- Không có kiểm soát input đầu vào: Ví dụ, nếu developer chỉ chấp nhận đối tượng thuộc một số class nhất định hoặc kiểm tra chặt chẽ đầu vào trước khi unserialize, việc khai thác sẽ khó khăn hơn. Trên thực tế, cơ chế giới hạn lớp rất hiếm khi được triển khai đúng. OWASP cũng nhấn mạnh rằng gần như "không thể giải tuần tự dữ liệu không tin cậy một cách an toàn tuyệt đối" – nghĩa là chỉ cần thỏa điều kiện có deserialization input, ta nên giả định có lỗ hổng.
II. Phân tích cách khai thác
Kẻ tấn công sẽ tìm cách “luồn” payload vào ứng dụng thông qua điểm nhập phù hợp, rồi lợi dụng quá trình giải tuần tự để kích hoạt một hành vi ngoài ý muốn trong ứng dụng (thường là thực thi mã). Ta phân tích các yếu tố: điểm nhập (entry point), điểm thực thi (execution sink), và dòng chảy của payload qua các bước xử lý.
Entry point (Điểm nhập dữ liệu - source)
Điểm nhập phổ biến cho chuỗi tuần tự hóa là các tham số HTTP có thể tùy ý chỉnh sửa. Ví dụ điển hình là cookie phiên (session cookie): nhiều ứng dụng web (PHP, Java, thậm chí một số framewok Ruby) từng sử dụng cookie chứa dữ liệu người dùng đã tuần tự hóa. Trong lab của PortSwigger, cookie phiên được mã hóa URL và Base64, thực chất chứa một đối tượng PHP đã tuần tự hóa. Kẻ tấn công có thể giải mã cookie này, sửa đổi nội dung rồi mã hóa lại và gửi lên – server sẽ unserialize chuỗi dữ liệu đã bị chỉnh sửa.
Một điểm nhập khác là tham số API/HTTP trực tiếp. Ví dụ, một ứng dụng Ruby on Rails có thể nhận tham số data chứa chuỗi Base64 của một đối tượng Marshal. Đoạn code giả lập dưới đây cho thấy controller Rails đọc tham số và gọi Marshal.load mà không có kiểm tra:

Trong trường hợp trên, bất kỳ ai gửi một chuỗi byte Marshal độc hại (sau khi Base64) vào
params[:data] đều có thể ép server tạo ra đối tượng tùy ý – Rails coi đó là dữ liệu hợp lệ và nạp vào mà không biết đó là bẫy. Nghiên cứu đã chỉ ra rằng truyền input không tin cậy vào Marshal.load nên được coi là lỗ hổng RCE nghiêm trọng.Đối với Java, entry point có thể ít rõ ràng hơn trên web, nhưng vẫn tồn tại. Ví dụ: một ứng dụng Java web có thể nhận một tham số Base64 và dùng
ObjectInputStream để đọc đối tượng (giả sử cho tính năng “ghi nhớ đăng nhập”). Nếu tham số này không được xác thực, đó là cửa vào cho payload. Một đoạn pseudo-code Java minh họa:
Trong đoạn code Java trên, việc gọi
ois.readObject() trên dữ liệu từ bên ngoài chính là điểm nhập lỗ hổng. Kẻ tấn công có thể gửi một chuỗi đã serialize (dạng binary, được base64) chứa payload. Khi readObject() thực thi, object độc hại sẽ được tạo ra.Ngoài cookie và tham số, một điểm nhập khác ở PHP là qua
phar:// stream. PHP cho phép các wrapper giao thức đặc biệt cho hàm file. Đáng chú ý, file .phar (PHP Archive) có chứa metadata được tuần tự hóa. Nếu ứng dụng gọi bất kỳ hàm file system nào (như file_exists, include, exif_read_data...) lên một đường dẫn bắt đầu bằng phar://, PHP sẽ ngầm giải tuần tự metadata của file PHAR đó. Kẻ tấn công có thể lợi dụng bằng cách upload một file .phar (hoặc file polyglot .jpg+.phar), rồi tìm chỗ nào ứng dụng đọc file từ đường dẫn mình kiểm soát để trỏ tới phar://<file độc hại>. Khi đó, payload trong metadata sẽ được unserialize và thực thi. Entry point này ít trực tiếp hơn (phải có chức năng upload và file operation), nhưng rất nguy hiểm vì nó bỏ qua hoàn toàn tầng logic ứng dụng – PHP tự động xử lý.Tóm lại, kẻ tấn công sẽ xác định mọi nơi dữ liệu từ client được unserialize. Cookie, tham số request, dữ liệu file... đều là các entry point tiềm năng. Điều quan trọng là dữ liệu đó đến từ bên ngoài và chưa được bảo vệ (không được ký HMAC, không bị mã hóa tránh sửa, hoặc cơ chế bảo vệ có tồn tại nhưng bị lộ key hoặc dễ đoán).
Execution sink (Điểm thực thi payload)
Execution sink là nơi payload thực sự gây ra hành vi độc hại – hiểu đơn giản là đích đến cuối cùng của chuỗi tấn công. Trong lỗ hổng deserialization, sink thường là đoạn code nguy hiểm được kích hoạt trong quá trình tạo đối tượng hoặc ngay sau khi đối tượng được tạo. Có hai trường hợp phổ biến:
- Magic methods tự động thực thi: Nhiều ngôn ngữ cung cấp các Magic methods (phương thức “ma thuật”) được gọi tự động khi một sự kiện xảy ra. Trong ngữ cảnh này, sự kiện chính là đối tượng được tạo ra do deserialization. Ví dụ, PHP tự động gọi
__wakeup() sau khi unserialize, và gọi __destruct() khi object bị giải phóng (kết thúc tiến trình). Java cho phép khai báo method private void readObject(...) bên trong lớp, method này sẽ tự động chạy khi đối tượng được deserialize. Ruby khi load YAML hoặc Marshal cũng sẽ thực thi một số hàm khởi tạo đối tượng và có thể tận dụng các phương thức như yaml_initialize hoặc các hook của thư viện (như trong chuỗi gadget của Ruby YAML, method Kernel#system được gọi thông qua chuỗi đối tượng trong quá trình load. Những magic method này chính là điểm thực thi đầu tiên mà kẻ tấn công hướng tới. Nếu chúng chứa lệnh nguy hiểm (VD: gọi system(), exec(), xóa file, ghi file, query DB tùy ý...), thì chỉ cần object độc hại được tạo ra, các lệnh đó sẽ tự động chạy với quyền của ứng dụng.- Hành vi nguy hiểm khi ứng dụng tương tác với object: Không phải lúc nào payload cũng kích hoạt hoàn toàn trong quá trình unserialize. Đôi khi, đối tượng cần đi vào luồng xử lý chính và được sử dụng ở đâu đó để gây hại. Ví dụ, giả sử
object User có thuộc tính isAdmin. Sau khi unserialize, ứng dụng kiểm tra if($user->isAdmin) ... để quyết định cho vào trang quản trị. Nếu kẻ tấn công đã sửa $user->isAdmin=true, thì điểm sink ở đây là đoạn code kiểm tra quyền – nó sẽ mở quyền admin sai mục đích. Trong lab PortSwigger, sau khi đổi thuộc tính admin từ false thành true trong cookie, phản hồi từ server xuất hiện chức năng admin (link /admin) chứng tỏ ứng dụng tin rằng người dùng có quyền quản trị. Từ đó, kẻ tấn công truy cập được trang admin và xóa được tài khoản người khác (ví dụ user carlos) dù ban đầu đăng nhập với user thường. Trường hợp này, sink là lỗ hổng thiếu kiểm tra quyền hạn do dữ liệu tin tưởng sai, chứ không phải thực thi mã tùy ý. Tuy nhiên, hậu quả bảo mật vẫn nghiêm trọng (leo thang đặc quyền).Như vậy, tùy mục đích, sink có thể là:
- Remote Code Execution (RCE): khi payload thành công gọi được các hàm như
system(), exec() (PHP), Runtime.getRuntime().exec() (Java), hay ` backtick/system(Ruby) để thực thi lệnh OS; hoặc chèn mã vào ứng dụng (như một số gadget cho phép viết file PHP và sau đó include). Đây thường là mục tiêu cao nhất. - Arbitrary File Access/Modification: đọc hoặc ghi/xóa file tùy ý. Ví dụ, gadget xóa file (như ví dụ
User->deleteImage() của PortSwigger xóa file theo đường dẫn trong thuộc tính, kẻ tấn công có thể chỉnh path để xóa file quan trọng). - Privilege Escalation / Logic Manipulation: thay đổi dữ liệu nhạy cảm trong object (role, ID, số dư, …) để qua mặt các kiểm tra logic. Như đã nêu, đổi trườngisAdminhoặcuserId` trong đối tượng có thể chiếm quyền hoặc truy cập nhầm dữ liệu.
- Denial of Service (DoS): tạo các object phức tạp gây cạn tài nguyên khi unserialize (VD: cấu trúc đệ quy gây loop, hoặc chuỗi gây allocate bộ nhớ lớn). Thậm chí nếu không tìm được cách RCE, kẻ tấn công có thể làm ứng dụng treo hoặc crash bằng payload đặc biệt.
Trong quá trình tấn công, payload thường được mã hóa hoặc biến đổi để phù hợp với kênh đầu vào. Ví dụ, chuỗi serialized thường chứa ký tự binary nên hay được Base64 encode để gửi qua HTTP an toàn. Thật vậy, cookie trong ví dụ trên đã được Base64 (và URL encode) trước khi gửi. Điều này vừa phục vụ kỹ thuật (tránh ký tự null, control trong HTTP), vừa có lợi cho kẻ tấn công: encoding giúp bypass một số WAF chặn chuỗi đáng ngờ. Nhiều WAF có thể nhận diện pattern như O: (mở đầu chuỗi object PHP) và chặn request. Nếu payload được Base64, WAF có thể không phát hiện, trong khi server sẽ decode và unserialize bình thường.
Một số ứng dụng triển khai chữ ký số hoặc checksum để bảo vệ dữ liệu tuần tự hóa (ví dụ thêm HMAC vào cookie). Khi đó, việc sửa đổi nhỏ cũng làm chữ ký sai và server sẽ phát hiện. Tuy nhiên, cơ chế này chỉ an toàn nếu khóa bí mật giữ kín. Trong thực tế, kẻ tấn công có thể tìm cách lấy được khóa (thông qua lộ lọt thông tin hoặc brute-force nếu khóa yếu). Chẳng hạn, có trường hợp cookie chứa
JSON {"token":"<data>", "sig":"<HMAC>"} dùng key trong biến môi trường. Nếu ứng dụng vô tình lộ SECRET_KEY (vd. qua trang debug như phpinfo), kẻ tấn công sẽ tính được HMAC mới cho payload của họ. Điều kiện có bảo vệ nhưng bảo vệ bị bypass như vậy đưa ta trở lại tình huống lỗ hổng có thể khai thác.=> payload chạm vào đâu phụ thuộc vào gadget mà kẻ tấn công sử dụng. Có thể là ngay trong hàm magic của đối tượng (RCE tức thì), hoặc trong các đoạn code xử lý object sau đó (đổi quyền, xóa file, ...). Execution sink chính là nơi ta quan sát được tác động: ví dụ lệnh OS được chạy (thấy kết quả hoặc side-effect như file bị xóa), hoặc ứng dụng thay đổi hành vi (cấp quyền sai, treo máy, v.v.).
Payload flow (Dòng chảy của payload)

Minh họa chuỗi tấn công qua deserialization: Kẻ tấn công gửi một đối tượng độc hại đã được tuần tự hóa. (src: portswigger)
Dòng chảy của payload có thể tóm tắt như sau:
(1) Dữ liệu do kẻ tấn công kiểm soát đi vào ứng dụng qua điểm nhập (cookie, tham số...).
(2) Dữ liệu được decode/giải mã nếu cần (ví dụ Base64 decode).
(3) Ứng dụng gọi hàm deserialization, biến chuỗi byte thành đối tượng.(4) Trong quá trình này, nếu đối tượng có phương thức đặc biệt (__wakeup, readObject, v.v.), chúng được tự động gọi – đây là lúc payload bắt đầu thực thi.
(5) Các phương thức này có thể tự thực hiện hành động nguy hiểm, hoặc gọi sang các đối tượng khác (gadget khác).
(6) Chuỗi các lời gọi (method invocations) tiếp diễn, truyền dữ liệu của kẻ tấn công qua nhiều lớp. Cuối cùng dữ liệu đến một sink gadget – một phương thức cuối cùng thực hiện hành động nguy hiểm nhất (ví dụ gọi system(cmd) hoặc ghi payload vào file và include)
Nếu so sánh, quá trình này giống hiệu ứng domino: object ban đầu khi được nạp sẽ kích hoạt một loạt các object khác (đã tồn tại sẵn trong ứng dụng) tương tác với nhau theo kịch bản mà kẻ tấn công sắp đặt bằng cách điều khiển dữ liệu đầu vào. Điều đáng nói: toàn bộ chuỗi code này vốn đã có sẵn trên server (trong code ứng dụng hoặc thư viện) – kẻ tấn công không tải lên mã mới, mà chỉ lợi dụng dòng chảy thực thi để đạt mục đích. Do đó rất khó để phát hiện chỉ bằng cách xem qua mã nguồn, vì từng phần code có thể trông vô hại cho đến khi được xâu chuỗi với nhau.
Một payload deserialization phức tạp có thể qua nhiều tầng chuyển đổi. Ví dụ: payload Java có thể gồm đối tượng A, trong A chứa thuộc tính là đối tượng B, B chứa đối tượng C,... Mỗi lần tạo một object, các hàm khởi tạo hoặc
readObject của nó lại chạy một phần. Chuỗi gadget có thể dài hoặc ngắn tùy mục tiêu. Có trường hợp chỉ cần 1-2 bước (như ví dụ đổi trường admin chỉ cần unserialize xong dùng luôn), nhưng để thực thi mã thường cần chuỗi dài hơn (gọi nhiều phương thức nối tiếp).Quan trọng là, tất cả diễn ra trong một lần deserialization (thường trong một request). Ứng dụng không hề gọi hàm nguy hiểm trực tiếp; thay vào đó, payload “dắt mũi” luồng thực thi đi đến chỗ nguy hiểm. Hiểu rõ dòng chảy này giúp ta xây dựng và tùy biến payload hiệu quả hơn.

Khoa học - Công nghệ
/khoa-hoc-cong-nghe
Bài viết nổi bật khác
- Hot nhất
- Mới nhất

