Bài này mình sẽ viết về những thứ mà mình học được về lỗ hổng Insecure Deserialization trong PHP nhằm ôn lại và củng cố thêm kiến thức. Nếu có sai sót ở đâu mong mọi người góp ý và bỏ qua (^_^)
Trước hết tìm hiểu về khái niệm serialization và deserialization.
Như vậy lỗ hổng Insecure Deserialization xảy ra khi thực hiện quá trình deserialization. Nguyên nhân chính là do việc ứng dụng web để người dùng có thể kiểm soát được các dữ liệu sau khi serialize đối tượng nào đó (gọi là serialized data), khi thay đổi chúng ứng dụng sẽ thực hiện quá trình deserialization để khôi phục lại đối tượng ban đầu và đương nhiên đối tượng đó sẽ bị thay đổi và có thể gây ra ảnh hưởng tới ứng dụng, thậm chí là máy chủ chứ không chỉ đối tượng đó.
Trong PHP, thực hiện quá trình serialization bằng hàm serialize(), và quá trình deserialization bằng hàm unserialize().
Nói 1 chút về định dạng của serialized data:
Ví dụ: có 1 đối tượng là $user
có 3 thuộc tính:
Sau khi thực hiện serialize bằng hàm serialize($user)
thu được kết quả:
O:4:"User":3
: O
là object có class tên là User
và độ dài tên class là 4
, object này có 3
thuộc tính.s:4:"name";s:6:"chuong"
: đây là thuộc tính đầu tiên có kiểu dữ liệu là string (s
), có tên là name
với độ dài là 4
, giá trị là string (s
) có giá trị là chuong
với độ dài là 6
.s:3:"age";i:18
: đây là thuộc tính thứ 2 có dạng string (s
), tên thuộc tính là age
có 3
ký tự và giá trị là 18
với kiểu dữ liệu int (i
).s:7:"isAdmin";b:0
: đây là thuộc tính cuối cùng tương tự trên và có giá trị boolen (b
) với giá trị false (0
).Đây là dạng bài đơn giản nhất.
Giả sử ứng dụng web kiểm tra quyền admin bằng cách kiểm tra giá trị isAdmin
(ví dụ trên). Nếu giá trị là true sẽ được cấp quyền và ngược lại không nếu là false.
Vậy khi người dùng có thể kiểm soát serialized data sẽ chỉnh sửa được giá trị false về true bằng cách thay đổi s:7:"isAdmin";b:0
thành s:7:"isAdmin";b:1
.
Khi ứng dụng deserialize
lại nhận được đối tượng giá giá trị thuộc tính isAdmin
là true => thành công.
Giả sử ứng dụng có chức năng đọc file bằng hàm file_get_contents()
:
Như đã nói ở dạng trên, kẻ tấn công có thể thay đổi các thuộc tính và khi đó $this->name
sẽ bị thay đổi thành các file nhạy cảm như /etc/passwd
,… dẫn đến việc kẻ tấn công có thể đọc được chúng.
Khi các ứng dụng sử dụng cách xác thực tài khoản không an toàn:
Ở đây thứ mà ta kiểm soát được là username
và password
, thứ chưa biết là $adminPassword
. Vậy làm sao để bypass cái này?. Đó là dạng type juggling khi ứng dụng sử dụng so sánh loose (==
) mà không phải ===
. Khi so sánh chuỗi (ở đây là $adminPassword
) với boolean mang giá trị true
sẽ trả về kết quả đúng. Vậy chỉ việc thay đổi giá trị password
thành kiểu boolean mang giá trị true
là được.
Tham khảo thêm về type juggling: https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf
POP chain còn được gọi là Code Reuse Attack, đây là 1 kỹ thuật liên quan đến việc sử dụng lại các đoạn code của chương trình (gọi là các gadget) để liên kết chúng lại thành 1 chuỗi thực thi (chain) đồng thời kết hợp với việc thay đổi các thuộc tính của các đối tượng tạo ra một luồng hoạt động với mục đích tấn công ứng dụng.
Quan trọng của kỹ thuật này là việc sử dụng các magic method (các method bắt đầu bằng __
), đại khái là nó sẽ được thực thi tự động khi thỏa mãn điều kiện, cụ thể với một số như sau:
__construct()
: method này sẽ thực thi khi khởi tạo đối tượng từ class, ví dụ: $user= new User('chuong');
.__destruct()
: method này sẽ thực thi khi đối tượng bị hủy ví dụ như dùng hàm unset($user)
hoặc khi chương trình kết thúc (ngoài trừ việc chương trình kết thúc bằng 1 số hàm như die()
).__wakeup()
: method này sẽ thực thi khi thực hiện quá trình deserialization, ví dụ: unserialize($user);
.__toString()
: method này sẽ thực thi khi đối tượng được sử dụng như chuỗi , ví dụ như sử dụng các hàm in ra chuỗi như echo
, print()
,… hoặc sử dụng so sánh loose (==
), hoặc trong các hàm như preg_match()
.__invoke()
: method này sẽ thực thi khi đối tượng được gọi như một hàm, ví dụ: $user();
.Tiếp theo thì phải chú ý đến việc ứng dụng sử dụng các function đặc biệt được sử dụng như: file_get_contents()
, include()
, system()
, unlink()
,..
Đó có thể là các filesystem functions để thao tác với các file hệ thống cũng như hệ thống gây nên các cuộc tấn công. Ví dụ như chương trình sử dụng file_get_contents()
để đọc file nào đó, khi kẻ tấn công thay đổi các thuộc tính truyền vào vào hàm đó như /etc/passwd
nghĩa là có thể đọc được nó cũng như các file nhạy cảm khác. Và tất nhiên để ứng dụng làm được vậy có thể phải nhờ đến việc sử dụng các đoạn code khác trong chương trình (gadgets).
Để xem các gadgets liên kết với nhau ra sao thì mình sẽ lấy một ví dụ đơn giản:
Có file index.php
có 2 class A
, B
và nhận vào biến hix
từ GET để unserialize() nó.
Nhìn sơ qua biết được class A
có function read()
để đọc file hello.txt
nhưng chúng ta muốn đọc file flag.txt
. Đơn giản thôi chỉ cần thay đổi thuộc tính của đối tượng tạo từ class A
là filename
có giá trị là flag.txt
. Nhưng làm sao để read()
được thực thi thì chú ý trong class B
có gọi read()
:
Giả sử tạo đối tượng $a
từ class A
:
$a=new A('flag.txt');
Vậy read()
thực thi khi xảy ra $a->read()
vậy $this->name
trong class B
là đối tượng tạo từ class A
(ở đây là $a
). Vậy chỉ cần tạo đối tượng $b
từ class B
có thuộc tính name
là $a
:
$b=new B($a);
Muốn read()
thực thi thì __wakeup()
cũng thực thi. Đây là magic method thực thi khi thực hiện quá trình deserialization (hàm unserialize()).
Vậy chỉ cần đưa chỉ cần đưa serialized data của $b
vào biến hix
từ GET.
Bây giờ chỉ cần viết script tạo nó :
…
Đó là 1 bài đơn giản và trong các bài CTF hay trong thực tế có thể có nhiều gadgets đòi hỏi chúng ta phải phân tích tìm con đường phù hợp để đạt được mục tiêu.
Mình đã làm 1 bài để giải các bài dạng này ở đây
Ngoài ra có thể dùng tool PHPGGC với POP chain có sẵn của nhiều thư viện.
Phar (PHP Archive) là một định dạng gói cho phép phân phối các ứng dụng và thư viện bằng cách gói các file PHP và một số file khác. Nó giống như một file được nén lại từ nhiều file và thành phần khác.
Cấu trúc của file Phar:
__HALT_COMPILER();
Ở phần cuối, nó lưu Meta-data dưới dạng serialized, vì vậy khi gọi đến file phar (phar://
) thì các data này sẽ unserialize => vector tấn công.
Như vậy là nó cũng thuộc dạng POP chain.
Tạo 1 file phar:
create_phar.php
, ví dụ như:Dùng command để thực thi: php -d "phar.readonly=0" create_phar.php
(Trong một số bài uploads file, nếu server kiểm tra các magic bytes ta có thể thêm nó phần đầu của Stub
)
Sau khi được file test.phar
, ta sử dụng nó vào input khai thác (các filesytem functions): phar://path/to/test.phar
Tham khảo thêm: