Java Internals & Concurrency/Deserialization — lỗ hổng RCE và alternatives JSON/Protobuf
43/75
Bài 43 / 75~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
0/5 câu đã trả lời
  1. Q1
    Vì sao deserialization từ input không tin là lỗ hổng nghiêm trọng đến vậy?
  2. Q2
    Filter "com.myapp.*;!*" của ObjectInputFilter hoạt động thế nào, và vì sao allowlist tốt hơn blocklist?
  3. Q3
    Vì sao JSON/Protobuf không có lỗ hổng code-execution kiểu gadget chain như native serialization?
  4. Q4
    Vì sao hiện nay team production chuyển sang JSON/Protobuf thay Java native serialization?
  5. 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?

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

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Bài tiếp theo

Stream file — Files.lines, memory-mapped, channel