Java Internals & Concurrency/Deserialization — lỗ hổng RCE và alternatives JSON/Protobuf
24/39
Bài 24 / 39~13 phútI/O & NIOMiễn phí lượt xem

Deserialization — lỗ hổng RCE và alternatives JSON/Protobuf

Gadget chain biến ObjectInputStream.readObject thành remote code execution — câu chuyện 'Java deserialization apocalypse' 2015. Phòng chống với ObjectInputFilter (JEP 290) và alternatives: JSON, Protobuf, Avro.

TL;DR: ObjectInputStream.readObject() không chỉ đọc data — nó load class, tạo instance và chạy readObject của class đó. Attacker không cần class độc trên server: chỉ cần chain các class có sẵn trong classpath (gadget chain, nổi tiếng nhất là InvokerTransformer của Apache Commons Collections) để dẫn tới Runtime.exec — RCE (Remote Code Execution: kẻ tấn công chạy code tuỳ ý trên server từ xa). Năm 2015 kỹ thuật này quét qua WebSphere, JBoss, Jenkins, WebLogic. Phòng chống theo thứ tự: không deserialize input không tin; ObjectInputFilter allowlist (JEP 290, Java 9+); và tốt nhất — chuyển sang format data thuần như JSON hoặc Protobuf, không có khái niệm "execute code khi parse".

Tháng 1/2015, tại hội thảo AppSec California, hai researcher Chris Frohoff và Gabriel Lawrence trình bày talk "Marshalling Pickles — how deserializing objects can ruin your day" — demo cách craft byte stream mà khi deserialize sẽ kích hoạt chuỗi class có sẵn trong Apache Commons Collections, dẫn tới thực thi arbitrary code. Talk lúc đó chưa gây bão.

Cơn bão đến tháng 11/2015, khi Foxglove Security publish blog post áp kỹ thuật này vào sản phẩm thật: WebSphere, JBoss, Jenkins, WebLogic, OpenNMS — chứng minh chỉ cần gửi 1 byte array tới endpoint nhận ObjectInputStream là thực thi code trên server. Không cần login, không cần exploit web framework cụ thể — chỉ cần classpath có Commons Collections (gần như mọi Java app enterprise lúc đó). Industry gọi sự kiện này là "Java deserialization apocalypse": CVE-2015-4852 (WebLogic), CVE-2015-7501 (JBoss/Jenkins), CVE-2015-8103 — hàng chục vendor phát hành emergency patch. Các năm sau tiếp tục xuất hiện gadget chain mới: Spring, Groovy, Hibernate, BeanShell.

Gốc rễ: Java native serialization (bài trước) có flaw cơ bản — quá trình deserialize thực thi code của class được deserialize. Phía JDK: từ 2013, JEP 154 từng đề xuất remove serialization khỏi Java (bị rút lại — chủ yếu mang tính đánh dấu vấn đề). Sau sự kiện 2015, Java 9 thêm cơ chế filter ObjectInputFilter qua JEP 290. Serialization vẫn chưa deprecated chính thức, nhưng team JDK khuyến nghị mạnh: code mới không dùng native serialization cho dữ liệu từ bên ngoài.

Bài này giải thích: cơ chế gadget chain, các CVE tiêu biểu, cách phòng chống với ObjectInputFilter, và alternatives hiện đại (JSON, Protobuf, Avro).

1. Cơ chế gadget chain

Khi ObjectInputStream.readObject() chạy, nó:

  1. Đọc class name từ byte stream.
  2. Load class từ classpath.
  3. Dùng reflection tạo instance (bỏ qua constructor bình thường).
  4. Set field từ byte stream.
  5. Gọi readObject private method của class nếu có.

Bước 5 là lỗ hổng: readObjectcode — nó chạy trong JVM với quyền của process. Nếu class có readObject với logic "nguy hiểm" (vd Runtime.exec), deserialize triggers execution.

Nhưng attacker không control class trên server — làm sao? Dùng gadget chain: chain class có sẵn trên classpath, kết hợp để cuối cùng gọi Runtime.exec.

Gadget chain nổi tiếng — Apache Commons Collections InvokerTransformer:

// Byte craft boi attacker deserialize thanh:
Transformer[] chain = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", ..., {"exec", String.class}),
    new InvokerTransformer("invoke", ..., {null, ...}),
    new InvokerTransformer("exec", ..., {"rm -rf /"}),
};
// Khi deserialize, chain kich hoat -> Runtime.exec("rm -rf /")

Attacker gửi byte này tới endpoint nào accept ObjectInputStream (vd old RMI, old SOAP endpoint, serialization cache) → server execute command.

2. CVE nổi bật

  • CVE-2015-4852 (Oracle WebLogic) — RCE qua T3 protocol.
  • CVE-2015-7501 (JBoss, Jenkins) — RCE qua JMX/remote channel.
  • CVE-2016-1000027 (Spring) — Spring MVC RemoteInvocationSerializingExporter.
  • CVE-2019-2725 (Oracle WebLogic) — lỗ hổng nghiêm trọng, Oracle emergency patch.
  • Hàng chục khác — Hibernate, Groovy, BeanShell, Commons-FileUpload.

Tất cả cùng pattern: deserialize byte từ input không tin → gadget chain → RCE.

3. Phòng chống

Cách 1 — không deserialize untrusted input. Đây là biện pháp số 1.

Cách 2 — Allowlist filter (Java 9+, JEP 290):

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.User;com.myapp.Order;!*"
);

ObjectInputStream ois = new ObjectInputStream(source);
ois.setObjectInputFilter(filter);
User u = (User) ois.readObject();

Pattern "com.myapp.User;com.myapp.Order;!*":

  • com.myapp.User — cho phép.
  • com.myapp.Order — cho phép.
  • !* — từ chối mọi class khác.

Chỉ class trong allowlist được deserialize. Gadget chain với InvokerTransformer bị reject.

Java 8 không có ObjectInputFilter — dùng library SerialKiller (third-party) hoặc upgrade Java.

Cách 3 — dùng format khác hoàn toàn (JSON, Protobuf) — xem mục 4.

⚠️ Quy tắc vàng

Không deserialize byte từ nguồn không tin. Nếu phải serialize cross-process/network, dùng JSON hoặc Protobuf — format data thuần, không có concept "execute class's readObject" — không có code execution surface.

4. Alternatives — JSON và Protobuf

Hiện nay team production tránh Java native serialization, chuyển sang text/binary format trung lập.

JSON với Jackson

Thư viện phổ biến: Jackson (default trong Spring), Gson (Google).

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();

// Serialize
User u = new User("Alice", 30);
String json = mapper.writeValueAsString(u);
// {"name":"Alice","age":30}

// Deserialize
User u2 = mapper.readValue(json, User.class);

Ưu điểm:

  • Human-readable: debug, inspect, modify bằng tay.
  • Không executable class: format data thuần, không concept execute readObject.
  • Cross-language: Python, Go, JavaScript, Rust cùng đọc được.
  • Không yêu cầu Serializable: class thường là OK.
  • Schema-less: thêm field mới không break client cũ (skip unknown).

Nhược:

  • Chậm hơn binary — Protobuf parse nhanh hơn ~5-20× tuỳ payload.
  • Size lớn hơn binary ~2-5×.
  • Không type-safe (JSON number parse thành int/long/double tuỳ config).

Dùng cho: REST API, config file, log format, cache với debug requirement.

Protobuf cho high-performance

Google Protocol Buffers — binary format với schema.

// user.proto
syntax = "proto3";

message User {
    string name = 1;
    int32 age = 2;
}

Codegen Java class từ schema:

protoc --java_out=src/main/java user.proto
User u = User.newBuilder()
    .setName("Alice")
    .setAge(30)
    .build();

byte[] bytes = u.toByteArray();
User u2 = User.parseFrom(bytes);

Ưu điểm:

  • Nhanh và gọn: parse nhanh hơn JSON ~5-20×, payload nhỏ hơn ~2-5× (cùng bộ số ở trên, nhìn từ phía ngược lại).
  • Schema evolution: thêm field mới backward-compatible (field number).
  • Không code execution: binary data thuần.
  • Cross-language: codegen Python, Go, C++, Rust.
  • Forward compatibility: client cũ parse được message mới (skip unknown field).

Nhược:

  • Cần .proto schema — thêm build step.
  • Binary không human-readable — debug cần tool.
  • Ecosystem learning curve.

Dùng cho: gRPC (default), high-throughput RPC, microservice communication, data pipeline (Kafka).

Avro — schema evolution friendly

Similar Protobuf nhưng:

  • Schema stored cùng data trong header → không cần distribute schema ngoài.
  • Hadoop native — file format cho big data.
  • Schema registry (Confluent) cho Kafka.

Dùng cho: Kafka message, data lake, ETL pipeline.

MessagePack, CBOR

Binary JSON — nhanh hơn JSON 2-3×, không cần schema. Tradeoff: không nhanh bằng Protobuf, không phổ cập bằng JSON.

5. Pitfall tổng hợp

Nhầm 1: Deserialize input từ user/network.

byte[] data = request.getBody();
User u = (User) new ObjectInputStream(new ByteArrayInputStream(data)).readObject();
// Lo hong RCE neu classpath co gadget chain

✅ JSON parse với schema validation:

User u = mapper.readValue(request.getBody(), User.class);

Nhầm 2: Quên filter trong Java 9+ khi buộc phải dùng native serialization.

ObjectInputStream ois = new ObjectInputStream(source);
ois.readObject();   // Khong filter - vulnerable

ObjectInputFilter với allowlist:

ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter("com.myapp.*;!*"));

Nhầm 3: Dùng Java serialization cho cross-language.

// Python client khong doc duoc .ser file

✅ JSON / Protobuf cho interop.

Nhầm 4: Tin rằng "app nội bộ thì không sao".

// "Endpoint chi trong mang noi bo" -> attacker lateral movement van toi duoc

✅ Defense in depth: filter + allowlist ngay cả cho endpoint nội bộ; mạng nội bộ không phải trust boundary.

6. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

Ghi chú: Trình tự thời gian đáng nhớ: JEP 154 (2013, rút lại) → talk Marshalling Pickles (1/2015) → Foxglove Security blog post khai thác thực tế (11/2015) → JEP 290 filter (Java 9, 2017) → JEP 415 (Java 17). JDK team (Brian Goetz, Stuart Marks) nhiều lần cảnh báo công khai — code mới không nên tin vào Serializable cho dữ liệu vượt trust boundary. Đọc kỹ OWASP Cheat Sheet nếu dự án đang dùng native serialization.

7. Tóm tắt

  • Deserialization là lỗ hổng security nghiêm trọng: readObject chạy code của class; gadget chain với class trên classpath dẫn đến RCE.
  • Sự kiện 2015: talk Marshalling Pickles (1/2015) + Foxglove Security blog post (11/2015) → "Java deserialization apocalypse", hàng chục vendor emergency patch.
  • Phòng chống theo thứ tự: (1) không deserialize untrusted input; (2) ObjectInputFilter allowlist (JEP 290, Java 9+); (3) chuyển sang format khác.
  • JSON (Jackson, Gson) — human-readable, cross-language, không executable format. Dùng cho REST, config, log.
  • Protobuf — binary, parse nhanh hơn JSON ~5-20×, nhỏ hơn ~2-5×, schema evolution. Dùng cho gRPC, high-throughput RPC.
  • Avro — schema đi cùng data, Hadoop/Kafka native.
  • JEP 154 (2013) là tín hiệu sớm JDK team muốn bỏ native serialization; JEP 290/415 là giải pháp thực tế hiện tại. Code mới không rely native serialization dài hạn.

8. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao deserialization từ input không tin là lỗ hổng nghiêm trọng đến vậy?

Cơ chế: ObjectInputStream.readObject chạy readObject private của class deserialize (qua reflection bypass access check). Nếu class trong classpath có logic dangerous trong readObject, deserialize ≈ execute code.

Attacker không control class trên server, nhưng craft gadget chain — chain class có sẵn trên classpath:

  1. Class A có readObject gọi method của B.
  2. Method B gọi method của C.
  3. Method C cuối chain gọi Runtime.exec.

Apache Commons Collections InvokerTransformer là gadget nổi tiếng nhất — có trong classpath của đa số enterprise Java app năm 2015. Attacker gửi byte crafted → server execute arbitrary command → RCE.

Phòng:

  1. Không deserialize input untrusted — quan trọng nhất.
  2. ObjectInputFilter allowlist (JEP 290, Java 9+) — reject class không biết.
  3. Dùng JSON/Protobuf — format data thuần, không concept execute.

Java native serialization là một trong những flaw thiết kế lịch sử của Java — JEP 154 (2013) từng đề xuất bỏ hẳn.

Q2
Filter "com.myapp.*;!*" của ObjectInputFilter hoạt động thế nào, và vì sao allowlist tốt hơn blocklist?

Filter đánh giá pattern từ trái sang phải với mỗi class xuất hiện trong stream (kể cả class lồng bên trong object graph): com.myapp.* cho phép mọi class thuộc package đó; !* từ chối tất cả phần còn lại. Class bị reject → InvalidClassException trước khi bất kỳ readObject nào chạy.

Allowlist tốt hơn blocklist vì gadget chain là mục tiêu di động: chặn InvokerTransformer hôm nay, ngày mai researcher tìm ra gadget mới trong Groovy, Spring, JDK nội bộ (chuỗi ysoserial có hàng chục gadget). Blocklist luôn chạy sau attacker; allowlist mặc định từ chối mọi thứ chưa biết.

Ngoài tên class, filter còn chặn theo độ sâu graph, số reference, kích thước stream (maxdepth, maxrefs, maxbytes) — chống cả DoS qua object graph khổng lồ.

Java 17 (JEP 415) thêm filter factory — gắn filter khác nhau cho từng context thay vì 1 filter global toàn JVM.

Q3
Vì sao JSON/Protobuf không có lỗ hổng code-execution kiểu gadget chain như native serialization?

Khác biệt nằm ở model parse:

  • Native serialization: byte stream chứa tên class — server load class theo yêu cầu của input, rồi chạy readObject của class đó. Input quyết định code nào chạy.
  • JSON/Protobuf: parser đọc data thuần (string, number, nested structure) rồi map vào type do code server chỉ định (mapper.readValue(json, User.class)). Input không có quyền chọn class, không có hook code nào chạy trong lúc parse.

Lưu ý: JSON library vẫn có thể tự tạo ra lỗ hổng nếu bật polymorphic typing — vd Jackson enableDefaultTyping() (đã deprecated) cho phép JSON chỉ định class qua field @class → tái hiện đúng flaw của native serialization (CVE-2017-7525 và chuỗi CVE Jackson). Bài học: giữ nguyên tắc "input không bao giờ được chọn class", bất kể format.

Q4
Vì sao hiện nay team production chuyển sang JSON/Protobuf thay Java native serialization?

6 lý do chính:

  1. Security: JSON/Protobuf parse data thuần, không execute code trong deserialize. Attacker không craft được gadget chain RCE.
  2. Cross-language: Python, Go, Rust, JavaScript đọc JSON/Protobuf được. Java .ser chỉ Java.
  3. Stable: không phụ thuộc class binary compat — schema evolve được (JSON thêm field OK, Protobuf có field number).
  4. Human-readable (JSON): debug, inspect không cần tool special.
  5. Performance (Protobuf): payload nhỏ hơn ~2-5×, parse nhanh hơn nhiều lần so với text format.
  6. Ecosystem: schema registry, validation tool, documentation (OpenAPI, Protobuf schema) — mature.

Java native serialization vẫn dùng cho:

  • RMI, EJB legacy (đang phasedown).
  • Cache distributed (Infinispan, Hazelcast) mặc định — có thể config alternative.
  • Session replication Spring cũ.

Code mới microservice dùng JSON + OpenAPI hoặc gRPC + Protobuf. Không dùng Java native cho cross-service communication.

Q5
Hệ thống legacy có endpoint nhận ObjectInputStream từ client cũ, chưa thể bỏ ngay. Mitigate thế nào theo thứ tự ưu tiên?

Thứ tự thực tế:

  1. Gắn ObjectInputFilter allowlist ngay — thay đổi nhỏ nhất, chặn gadget chain hiệu quả nhất. Liệt kê đúng các class hợp lệ + !*; thêm maxdepth/maxbytes chống DoS. Java 8 → dùng SerialKiller hoặc ưu tiên upgrade JDK.
  2. Audit classpath — gỡ library có gadget đã biết không còn dùng (Commons Collections cũ, BeanShell...). Tool: ysoserial list các chain phổ biến để biết mình đang chứa gì.
  3. Thu hẹp network exposure — endpoint chỉ nhận từ IP/service đã biết; nhưng nhớ đây là lớp phụ, không phải lớp chính (mạng nội bộ không phải trust boundary).
  4. Lên kế hoạch migrate — chuyển protocol sang JSON/Protobuf có version song song, deprecate dần endpoint cũ. Đây là fix gốc; 3 bước trên chỉ mua thời gian.

Anti-pattern: chỉ làm bước 3 rồi coi như xong — mọi CVE 2015 đều nằm sau firewall của ai đó.

Bài tiếp theo: Stream file — Files.lines, memory-mapped, channel

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên