II. Lỗ hổng deserialization trong ngôn ngữ PHP (tiếp)
6. Vấn đề deserialization trong session
Mỗi khi người dùng truy cập một ứng dụng sẽ được coi là một phiên và tạo ra một giá trị session (thường được đặt trong cookie). Về phía server, dữ liệu session này được lưu trữ trong một biến toàn cục $_SESSION
. Đối với các yêu cầu (request) liên tiếp từ cùng một khách hàng, server cần quan tâm đến cơ chế lưu trữ session, có thể lưu trữ trên đĩa, trong bộ nhớ. Từ đó, server cần quy định cơ chế xử lý các giá trị session này nhằm xác thực người dùng.
Trong PHP, session.serialize_handler là một tùy chọn cấu hình cho phép ứng dụng quy định cụ thể loại trình xử lý nào sẽ được sử dụng để lưu trữ và quản lý session (thông qua serialize và deserialize). Các định dạng được sử dụng là: php_serialize, php và php_binary, wddx.
Với định dạng php, session được lưu trữ gồm ba bộ phận: key name + | + output of serialize(). Ví dụ với giá trị secret|s:5:"viblo";
key name của biến $_SESSION['secret'] |
ký tự \| |
giá trị sau khi sử dụng hàm serialize() |
---|---|---|
secret | \| |
s:5:"viblo"; |
Với định dạng php_binary sử dụng một trình xử lý serialize nhị phân của PHP để chuyển đổi dữ liệu phiên thành một chuỗi nhị phân. Session được lưu trữ gồm ba bộ phận: ký tự ASCII + key name + output of serialize(). Ví dụ với giá trị !__length_of_this_key_name_is_33__s:5:"viblo";
ký tự ASCII ứng với | key name của biến $_SESSION['__length_of_this_key_name_is_33__'] |
giá trị sau khi sử dụng hàm serialize() |
---|---|---|
! |
length_of_this_key_name_is_33 | s:5:"viblo"; |
Đối với định dạng php_serialize sử dụng serialize và unserialize của PHP để chuyển đổi dữ liệu phiên thành một chuỗi. Ví dụ: a:1:{s:6:"secret";s:5:"viblo";}
Bản thân các trình xử lý session của PHP không chứa lỗ hổng. Tuy nhiên, do PHP mặc định sử dụng định dạng php cho tùy chọn session.serialize_handler, nên trong quá trình xây dựng phần mềm, có thể xảy ra sự khác nhau của trường session.serialize_handler, dẫn đến dữ liệu không được xử lý deserialize đúng hướng, tạo ra lỗ hổng deserialization trong session.
Xét ví dụ ứng dụng chứa tệp index.php
quy định trình xử lý session php_serialize:
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["secret"]=$_GET["secret"];
Trong khi tệp welcome.php
sử dụng trình xử lý session mặc định php, class Vul()
sử dụng hàm eval()
trong phương thức __destruct()
session_start();
class Vul { var $hi; function __construct(){ $this->hi = 'echo "welcome!\n";'; } function __destruct() { eval($this->hi); }
} $info = new Vul();
Tại welcome.php
, server sử dụng php xử lý dữ liệu session từ nơi lưu trữ sẽ coi phần giá trị trước ký tự | là key name của biến toàn cục $_SESSION
và thực hiện deserialize toàn bộ giá trị phía sau |. Ví dụ, với dữ liệu session dạng A|B
, sẽ thu được $_SESSION['A']
và thực thi unserialize(B)
.
Do đó, kẻ tấn công có thể lợi dụng cơ chế serialize của trình xử lý php_serialize, kết hợp hàm eval()
của lớp Vul(), tạo ra payload: |O:3:"Vul":1:{s:2:"hi";s:13:"system("id");
Giá trị session được lưu trữ là:
a:1:{s:6:"secret";s:42:"|O:3:"Vul":1:{s:2:"hi";s:13:"system("id");";}
Khi truy cập welcome.php
, server thực hiện unserialize('O:3:"Vul":1:{s:2:"hi";s:13:"system("id");";}')
trả về kết quả command id
:
III. Lỗ hổng Deserialization trong ngôn ngữ Java
Trong thực tế, lỗ hổng Deserialization hầu hết xuất hiện và bị khai thác trong các ứng dụng xây dựng bằng ngôn ngữ Java. Với Java, dạng lỗ hổng này có "biểu hiện" phức tạp hơn, các bạn hãy tập trung nhé!
1. Serialization và Deserialization trong Java
File Person.java
khai báo một class Person()
với hai thuộc tính name
và age
:
import java.io.Serializable; public class Person implements Serializable { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; }
}
Thực hiện serialization trong file Ser.java
như sau:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream; public class Ser { public static void main(String args[]) throws IOException { Person user = new Person("John", 18); FileOutputStream fos = new FileOutputStream("out.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(user); oos.flush(); oos.close(); }
}
Trong đó, đối tượng fos
thuộc lớp FileOutputStream
khai báo vị trí tệp tin xuất dữ liệu; đối tượng oos
thuộc lớp ObjectOutputStream
truyền đối tượng fos
vừa tạo vào và cho phép ghi đối tượng sang tệp; cuối cùng thực hiện serialize và ghi vào tệp output.txt
bằng cách gọi phương thức writeObject()
. Kết quả thu được:
Để Deserialize, chúng ta thực hiện ngược lại với các lớp FileInputStream()
, ObjectInputStream()
và phương thức readObject()
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream; public class Deser { public static void main(String args[]) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream("out.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Person person = (Person) ois.readObject(); System.out.print("Name: " + person.name + "\nAge: " + person.age); }
}
Triển khai interface Serializable
Để thực hiện serialize một đối tượng, khi xây dựng đối tượng đó cần sử dụng từ khóa implements để triển khai interface Serializable
.
Jump tới interface Serializable
, chúng ta sẽ phát hiện một điều đặc biệt vì nó là một interface rỗng:
Vậy thì chắc hẳn nhiều bạn sẽ có thắc mắc "Tại sao khi xây dựng ngôn ngữ Java lại thiết kế một interface rỗng Serializable
, để khi muốn serialize đối tượng cần gọi đến nó, trong khi có thể loại bỏ nó và trực tiếp serialize các đối tượng?". Đúng vậy, thoạt nhìn thì có cảm giác cách làm này có vẻ hơi "cồng kềnh".
Để giải thích cặn kẽ điều này chúng ta cần đi sâu vào vấn đề cốt lõi liên quan tới Serialize trong ngôn ngữ Java, chúng ta có thể hiểu đơn giản theo hướng bảo mật: Nếu không tồn tại interface Serializable
thì tất cả đối tượng đều có thể thực hiện serialize, dẫn tới tiềm ẩn nguy cơ kẻ tấn công lợi dụng cơ chế serialization thực hiện hành động phá hoại.
2. Lỗ hổng Deserialization trong Java
Quay lại ví dụ trên, khi Deserialize chúng ta đã gọi phương thức readObject()
nhằm đọc dữ liệu từ tệp out.txt
. Phương thức này đóng vai trò quan trọng trong quá trình Deserialization. Trong xây dựng chương trình Java, phương thức readObject()
có thể được ghi đè. Chẳng hạn với lớp Person()
trong ví dụ trên:
import java.io.IOException;
import java.io.Serializable; public class Person implements Serializable { // ... private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime runtime = Runtime.getRuntime(); String[] cmd = {"calc.exe"}; runtime.exec(cmd); }
}
Đoạn code trên gọi phương thức ReadObject()
mặc định với in.defaultReadObject()
. Sử dụng đối tượng thuộc lớp Runtime()
thực hiện phương thức exec()
nhằm thực thi lệnh gọi calc.exe
. (Chú ý exec(Java.lang.String)
không còn được khuyên dùng nữa).
Lúc này, khi thực hiện Deserialize đối tượng đã bị ghi đè, hệ thống sử dụng phương thức ReadObject()
nguy hiểm, sẽ thực hiện lệnh calc.exe
:
Điều này cũng chứng tỏ phương thức ReadObject()
là điểm xảy ra lỗ hổng Deserialization trong Java. Khi kẻ tấn công có thể thay đổi/control phương thức này sẽ có thể khai thác lỗ hổng. Tất nhiên rất hiếm có lập trình viên có thể viết ra các đoạn mã "bất ổn" như vậy, các lỗ hổng Deserialization của Java trong thực tế thường phức tạp hơn. Ở phần tiếp theo chúng ta sẽ đi sâu hơn trong quá trình khai thác dạng lỗ hổng này.
Các tài liệu tham khảo
- https://portswigger.net/web-security/deserialization
- https://www.php.net/manual/en/session.configuration.php
- https://viblo.asia/p/lo-hong-java-deserialization-va-nhung-dieu-co-the-ban-chua-biet-djeZ1wR85Wz
- https://viblo.asia/p/qua-trinh-deserialization-trong-java-thuc-su-dien-ra-nhu-the-nao-Qpmleyp7lrd