Prefix
Khi phân tích lỗ hổng này, mình thấy bồi hồi vì nhớ lại những kỉ niệm ngày trước. Lúc bắt đầu đi làm thì mình mới năm 2, lúc ấy chẳng biết gì nhưng vẫn luôn mơ mộng về việc trở thành 1 pro, có hàng trăm cái CVE 10.0, nhưng việc research đối với mình thực sự quá khó. Trải qua 2 công ty đầu, không ai dạy mình cách research cũng như là như thế nào mới thực sự là khai thác lỗ hổng bảo mật.
Thế nhưng may mắn thay, cuộc đời mình đi đâu cũng có quý nhân phù trợ. Ở công ty hiện tại, lúc mình vào làm được 1 tháng thì công ty cũng tuyển được 1 ông anh top tier trong mảng Pentest + Researcher ở Việt Nam, trùng hợp thay lại là người hướng dẫn trực tiếp cho mình 🥴.
Mình có hỏi ông anh là Tại sao em thấy em làm lâu mà không pro lên nhỉ. Trong quá trình làm việc, khi anh gặp 1 bức tường anh cảm giác tìm mãi không tìm được cách vượt qua thì anh làm như thế nào, ông anh đã trả lời: "Đấy là do em đặt mục tiêu chưa đúng thôi. Ví dụ em đặt mục tiêu tìm ra CVE thì cái đấy là sai, em đang đặt một mục tiêu nó không rõ ràng và khó đạt được, không phụ thuộc hoàn toàn vào bản thân của mình vì sản phẩm ấy chưa chắc đã có lỗi. Thay vào đó em nên đặt mục tiêu là phân tích 1day hoặc là nắm vững một cái giao thức nào đó. Ví dụ như RPC anh nghĩ em chắc không hiểu rõ về cái đấy đâu". Nghe điều này xong mình thấy mừng rơi nước mắt. Còn không biết ông anh có nhớ bản thân đã từng nói vậy không 😓
Nhân tiện mình liên hệ đến câu chuyện này vì lỗ hổng lần này có liên quan đến giao thức RPC, và mình nghĩ kia cũng là một lời khuyên đúng đắn cho mọi người.
1. Tìm hiểu lỗ hổng
1.1. Mục tiêu
Apache Dubbo là một framework mã nguồn mở dùng để phát triển các hệ thống phân tán. Kiến trúc của Apache Dubbo bao gồm ba phần chính:
- Service Provider: cung cấp dịch vụ, triển khai các interface của service và đăng ký với Service Registry.
- Service Consumer: sử dụng dịch vụ, truy cập Service Registry để tìm kiếm các service provider và giao tiếp với chúng.
- Service Registry: lưu trữ các thông tin về service provider, giúp service consumer tìm kiếm các service provider.
Ngoài ra còn 2 phần hỗ trợ: Monitor có vai trò đếm số lần service được gọi, thời gian tiêu tốn và Container quản lý thời gian tồn tại của service.
Các phần này hoạt động với nhau để tạo ra một hệ thống phân tán, cho phép các service provider và service consumer truyền tải dữ liệu qua mạng một cách hiệu quả. Dubbo cũng cung cấp các tính năng khác như giao tiếp đồng bộ và bất đồng bộ, khả năng mở rộng, quản lý và giám sát, và nhiều tính năng khác để hỗ trợ phát triển ứng dụng phân tán.
Chi tiết hơn, vui lòng tham khảo tại https://dubbo.apache.org/en/docs/v2.7/user/preface/architecture/
1.2. Mô tả lỗ hổng
Lỗ hổng này được mô tả như sau: Lỗ hổng này là một lỗ hổng liên quan đến việc giải tuần tự hoá đối tượng không an toàn - Insecure Deserialization. Version ảnh hưởng là 2.7.8, bản fix lỗi là 2.7.9.
2. Phân tích
Theo mô tả lỗi, lỗ hổng nằm ở chức năng GenericFilter của service Provider. Filter Extension được cài đặt mặc định trong Apache Dubbo với vai trò chặn lại các cuộc gọi từ xa cho Provider và Consumer. Mỗi khi một phương thức từ xa được gọi thì Filter Extension cũng được thực thi và được response tuỳ theo kết quả filter. (Phân tích cái này làm mình nhớ đến cái Network Filter Driver mình làm trong đồ án tốt nghiệp, cơ chế cũng gần tương tự, một thời từng suýt bục dạ dày vì nó 😢 ).
2.1. Điểm kích hoạt lỗ hổng
GenericFilter là một filter implement từ Filter Interface, trong số các tham số được gọi bởi dịch vụ, có một tham số là một mảng byte Object
Tham số này sẽ được deserialize ở phía sau với một trong các phương thức deserialize bao gồm nativejava, bean, protobuf-json.
Có một chút thú vị ở chỗ trong quá khứ cũng đã có một số CVE của Dubbo tương tự lỗi này CVE-2021-25641, khi mà kẻ tấn công có thể tuỳ ý lựa chọn phương thức mà đối tượng có thể được deserialize (attacker chọn nativejava với rất nhiều chain) và để chặn tấn công, dev đã không cho phép người dùng có thể truyền trực tiếp đối tượng với phương thức giải tuần tự hoá là nativejava. Nhưng thực tế trong lỗ hổng này, nơi trigger lại là bộ lọc GenericFilter, trước khi đối tượng thực sự được thực hiện RPC Invocation.
2.2. Cách khai thác
Client giao tiếp với Service Provider bằng RPC, vì vậy ta phải gửi 1 RPC Request hợp lệ.
Mỗi thư viện RPC sẽ sử dụng một cấu trúc request khác nhau, với Apache Dubbo các bạn tham khảo thêm tại https://dubbo.apache.org/en/docs/v2.7/user/references/metadata/
Muốn gọi RPC đến Dubbo Provider nói riêng và các RPC Server khác nói chung, bắt buộc cần có một service hợp lệ. Trong Dubbo có sẵn một service là org.apache.dubbo.samples.metadatareport.local.xml.api.DemoService#sayHello.
Chi tiết về cách triển khai service các bạn xem thêm tại https://dubbo.apache.org/en/docs/v2.7/user/references/telnet/.
Có thể gói gọn về một RPC Request trong Dubbo gồm 3 phần: phần metadata bao gồm thông tin service, method name, version Dubbo, v.v..., phần args bao gồm các tham số cho method và phần attachments là thông tin thêm đính kèm.
Bỏ qua phần metadata, trong phần args, GenericFilter đã nêu rõ gồm 3 args
Args 1 là method name, trong trường hợp này phải set bằng sayHello.
Chương trình thực hiện kiểm tra method có tồn tại không, và bằng việc set method name là sayHello thì nó đã thực hiện cuộc gọi tới phương thức mặc định nêu trên
Args 2 là một string object. Hiện tại mình chưa thấy tác dụng của args này.
Args 3 là 1 Object, đây chính là object dùng để trigger lỗ hổng.
Attachment phải là "generic":"nativejava" để chương trình nhảy sang phần deserialize cho object bằng phương thức nativejava
Việc lựa chọn gadget chains thì tuỳ tình hình thực tế của hệ thống thật. Do Dubbo chỉ là base của hệ thống, trong quá trình xây dựng sản phẩm, dev sẽ thêm các thư viện khác vào.
Tóm lại, một RPC request hoàn chỉnh sẽ có phần body như sau:
{ "service_name": "org.apache.dubbo.samples.basic.api.DemoService", "method_name": "$invoke", "param_types": "Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;", "service_version": "", "args": ["sayHello", ["java.lang.String"], ["<Payload byte array>"] ], "attachment": { "generic": "nativejava" }
}
Về Poc mình sẽ update sau 🥴 hiện tại chưa có.
Postfix
Có thể RCE bằng cách sử dụng phương thức deserialize với bean, raw.return, ... nhưng đó thuộc về một CVE khác đi liền với CVE này nên mình không đề cập thêm. Cách khai thác khá tương tự với nhau.