Reproduce CVE-2024–23897

Reproduce CVE-2024–23897

Đây là một post cũ, mình chuyển từ github repo sang để github đỡ bị tạp nham hơn :p

Jenkins là một Java opensource dùng để thực hiện chức năng tích hợp liên tục, triển khai liên tục (CI/CD – Continuous Integration/Continuous Delivery) và xây dựng các tác vụ tự động hóa. Nó đóng vai trò như một web server tự động thực hiện các tác vụ build, test. Đại khái thì khi lập trình viên tích hợp code của mình vào dự án thì Jenkins sẽ tự động build và chạy test, nếu không có lỗi gì xảy ra thì sẽ đưa code vào triển khai. Điều này làm cho việc phát triển các ứng dụng nhanh chóng hơn.

Jenkins Access Control

Mặc định, nếu Jenkins được cài ở dạng recommended (được cài thêm các extension phổ biến) thì một webapp Jenkins sẽ có 2 loại user:

  • anonymous user

  • authenticated user, có quyền hạn như admin Và 4 loại permission:

  • anyone can do anything: cho phép bất kì user nào kể trên thực hiện bất kì thay đổi với project

  • logged-in users can do anything: cho phép những user đã đăng nhập thực hiện thay đổi lên project

  • legacy mode: chỉ người dùng được gán role "admin" được phép thay đổi project, còn lại đều chỉ được cấp read access

  • Matrix-based security: mỗi người dùng được cấu hình để có vai trò cụ thể (như bảng). Vai trò này có tác dụng ở tất cả project (account scope)

  • Project-based security: cũng như bảng trên, nhưng người dùng được cấu hình quyền hạn dựa trên project (project scope) Còn nếu không cài ở dạng recommended thì có thể Jenkins sẽ làm giảm đi số lượng permissions xuống, nhưhg vẫn giữ nguyên 2 loại users như trên.

Ngoài ra, Jenkins còn cho phép cấu hình quyền hạn của anonymous user bằng cách cấp thêm read access:

Tuy nhiên, với phiên bản Jenkins từ 2.441 trở xuống (hoặc 2.426.2 LTS trở xuống), việc có cấp quyền read cho anonymous user hay không trở nên vô nghĩa khi mà CVE-2024-23897 được phát hiện.

CVE-2024-23897

Một số điểm cần chú ý về CVE này:

  • Lỗ hổng này cho phép ngay cả anonymous user cũng có thể đọc được bất kì file nào trong máy chủ, với số lượng hữu hạn kí tự. Cụ thể, nếu một attacker sử dụng command line interface (CLI) để truy cập đến Jenkins và thêm kí tự '@' thì phần sau @ sẽ được xem như là đường dẫn 1 tệp tin, và thông báo lỗi trả về từ Jenkins sẽ tiết lộ một phần nội dung tệp tin đó.

  • Ngoài ra, Jenkins có lưu một số secret key ở dạng plaintext, vì thế nếu các key này được dùng cho mục đích như SSH hay là authorization code cho admin account thì có thể dẫn đến RCE.

Setup

  • Để setup thì chỉ cần một file Dockerfile đơn giản như sau:
FROM jenkins/jenkins:2.441-jdk17
EXPOSE 8080
EXPOSE 50000
  • Rồi chạy Docker build -t jenkins . là xong. Sau khi build xong, khởi tạo container và vào localhost port 8080:

image

  • Copy paste admin password được log ở trong docker ra xong sẽ đến phần install plugin. Ở đây mình chọn Install suggested plugins:

image

  • Tạo tài khoản admin. Nếu không thì dùng tài khoản admin default cũng được:

image

  • Vậy là xong

Exploit

  • Ở trang chủ của Jenkins chọn Manage Jenkins -> Jenkins CLI

  • Tải file jenkins-cli.jar về máy. File này dùng để connect đến Jenkins host thông qua CLI:

image

  • Khi chạy thử lệnh được cung cấp, ta thấy Jenkins báo lỗi:

image

  • Ta thử cung cấp thêm tham số cho lệnh help xem sao:

image

\=> exploit thành công

  • Khi đọc Available commands của Jenkins, ta thấy có tới hơn 70 lệnh khác nhau được Jenkins định nghĩa, tuy nhiên hầu hết đều yêu cầu người dùng phải có quyền Read (mặc định được tắt đối với anonymous user). Ví dụ như câu lệnh gọi Groovy shell sau:

image

  • Sau khi tự động hóa việc chạy 70 câu lệnh thì mình nhận thấy có 2 câu lệnh hữu dụng, gồm: who-am-ihelp, cho phép đọc tối đa 3 dòng trong 1 file (đối với anonymous user không có read access:

image

image

  • Lệnh who-am-i chỉ cho phép đọc dòng đầu tiên của tệp, nhưng như vậy là đủ để đọc /proc/self/environ:

image

  • Ở /var/jenkins_home và /var/jenkins_home/secrets có chứa một directory là secret, chứa khá nhiều key quan trọng trong hệ thống:

image

  • Theo document thì các key này đa số được lưu ở dạng plaintext có thể được dùng trong các việc như mã hóa AES, ... Tác động lên hệ thống nếu các key này lộ ra được liệt kê đầy đủ tại đây

Thử đọc master.key:

image

  • Như vậy, với lỗ hổng này, attacker có thể đọc được khá nhiều tập tin quan trọng bên trong Jenkins server.

  • Nếu như anonymous user được cấp read access thì có thể full read một file bất kì. Để full read thì ta dùng lệnh connect-node hoặc reload-jobs:

image

Phân tích lỗ hổng

  • Khi compare ver 2.441 với 2.442 thì ta thấy commit chủ yếu xảy ra ở hai file là CLIRegisterer.javaCLICommand.java source. Các CLI hander của Jenkins đều được define ở core\src\main\java\hudson\cli

    image

  • Khi command được user gửi từ CLI đến server thì sẽ được call đến class CLIRegisterer xử lí, dưới dạng tham số args của hàm main.

  • Ở hàm main thì có define một parser thông qua return value của phương thức bindMethod(). Phương thức này nhận một List<MethodBinder> object làm tham số, với MethodBinder là một class dùng để chuyển các cmd line arguments từ user input sang cho thư viện args4j parse:

    image

  • Ở trong phương thức bindMethod(), đầu tiên phương thức registerOptionHandlers() dùng để gọi các option handler tương ứng cho từng arg, sau đó nó tạo một CmdLineParser object. Các tham số của user sẽ được đưa vào một stack và sau đó tìm ra các method resolver tương ứng. Đến cuối cùng, các method resolver lại được pass sang cho thư viện args4j xử lí thông qua class MethodBinder:

    image

  • Sau khi xong phần parse command line arguments thì chương trình bắt đầu parse arguments:

    image

  • Tiếp tục trace vào method này, ta thấy nó làm các công việc như sau:

    image

    • Đầu tiên, method này check xem các tham số có rỗng không. Sau đó, trong khối lệnh if, nó kiểm tra syntax của các arguments qua method this.parserProperties.getAtSyntax()

    • Tiếp tục trace lên thuộc tính this.parserProperties, ta thấy rằng nó được khởi tạo bằng class method ParserProperties.defaults(). Khối lệnh if bên dưới chỉ đơn giản là sort lại các argument, tuy nhiên nó không được thực thi do Jenkins đã instantiate một object CmdLineParser với tham số là null:

      image

    • Trace vào ParserProperties.defaults(), ta thấy rằng method này lại gọi đến constructor của class ParserProperties, tuy nhiên thuộc tính atSyntax của class này lại được init là true, vì thế khối if của method parseArgument() nêu trên luôn được thực thi.

      image

    • Ở if code block trên, args từ user input lại được đưa thẳng vào method expandAtFiles(args). Trong method này, nó kiểm tra xem nếu có tham số nào bắt đầu bằng '@' thì thực hiện đọc file và parse các tham số có trong file đó. Tuy nhiên, nếu không parse được thì throw một error đi kèm với nội dung của file:

      image

    • Sau khi thực hiện parse arguments từ file xong, chương trình tiếp tục duyệt qua các tham số và parse chúng. Khi này thì nội dung được đọc từ file đã được đưa vào instance cmdLine, sau đó chúng sẽ được kiểm tra xem liệu các tham số có quá dài hay là không hợp lệ không. Để ý rằng nếu một trong hai lỗi đó xảy ra thì chương trình sẽ throw một message đi kèm với các tham số bị lỗi => nếu user input refer đến một file như /etc/passwd thì chắc chắn chương trình sẽ báo lỗi, kèm nội dung của file.

Tái tạo lỗ hổng trên localhost

  • Đầu tiên ta cần install thư viện args4j.

  • Lỗ hổng của Jenkins xảy ra bởi việc handle các error message không đúng cách. Ta chỉ cần 1 chương trình Java đơn giản như sau để tái tạo lại:

      import org.kohsuke.args4j.CmdLineException;
    
      public class TestFile {
          public static void main(String[] args) throws CmdLineException {
    
              // anonymous user input comes from CLI application
              String[] userInput = {"@file_path_here"};
    
              // the backend initialize a command line parser
              CmdParser parser = new CmdParser();
    
              // the parser parses user input, which includes character '@'
              parser.parse(userInput);
          }
      }
    
    • Chạy thử trên local, ta thấy rằng nội dung file đã được leak thông qua error message:

      image

Related: