Giới thiệu
Cuối năm 2021, team mình đã phát hiện ra lỗ hổng SQL Injection trong phần core của WordPress và mới đây thì WordPress cũng đã tung ra bản vá cho lỗi này, cho nên hôm nay mình viết bài này để chia sẻ về lỗ hổng mà team mình đã tìm được.
Nói qua một chút về wordpress thì nó là 1 CMS mã nguồn mở được sử dụng nhiều nhất trên thế giới. Được sử dụng nhiều vì tính tiện dụng của nó khi cho phép developers có thể tự xây dựng plugin và theme để quản lý website, phần core của wordpress sẽ cung cấp các function để plugin/theme gọi tới và sử dụng các chức năng mà wordpress đã cung cấp như format data, query DB, … Trong những class mà wordpress đã cung cấp thì chúng mình đã tìm thấy lỗi SQL Injection trong class mà WP cung cấp dùng để query DB: WP_Query.
Phân tích lỗi
Trong phiên bản 5.8.3, wordpress đã fix lỗi này, so sánh commit thay đổi thì có thể thấy trong function clean_query đã được thêm phần kiểm tra $query[‘field’] trước khi xử lý biến $query[‘terms’].
Function clean_query được gọi từ get_sql_for_clause. Đọc code của hàm sẽ thấy công việc của hàm này là tạo ra các clause cho điều kiện trong 1 query SQL, cụ thể công việc của nó sẽ là xử lý dữ liệu nhận được, ghép dữ liệu đó thành 1 điều kiện trong query SQL và trả nó lại cho hàm cha. Vậy nên có thể control được dữ liệu trả về của hàm này, đồng nghĩa với việc ta có thể control được query SQL và thực hiện SQL Injection.
Quay trở lại hàm clean_query, khi chưa có thay đổi này, mặc định các giá trị trong $query[‘terms’] sẽ chỉ được xóa trùng lặp và sau đó gọi đến $this->transform_query( $query, ‘term_taxonomy_id’ );.
Để tránh rơi vào if thì $query[‘taxonomy’] cần phải rỗng hoặc là 1 giá trị để is_taxonomy_hierarchical return false.
Ở function transform_query sẽ kiểm tra $query[‘field’] == $resulting_field, nếu đúng sẽ return và không xử lý gì thêm, vậy nên nếu biến $query[‘field’] là term_taxonomy_id thì chúng ta có thể thoát khỏi hàm mà không làm thay đổi giá trị biến $query[‘terms’].
(So sánh ở đây đang sử dụng== và bị lỗ hổng Loose comparisons, trong 1 số trường hợp có thể sử dụng lỗi này để tạo ra 1 câu điều kiện theo ý muốn).
Sau khi thoát khỏi hàm, luồng code sẽ trở về vị trí được gọi hàm clean_query là hàm get_sql_for_clause, giá trị trong biến $query[‘terms’] sẽ được sử dụng trực tiếp làm điều kiện của query SQL và dẫn đến SQL Injection.
Vậy tóm lại, để có thể xảy ra SQL Injection cần có 2 điều kiện sau:
- $query[‘field’] là term_taxonomy_id
- $query[‘taxonomy’] trống hoặc is_taxonomy_hierarchical($query[‘taxonomy’]) === false
Flow dẫn đến lỗi như sau:
Khai thác
Mặc dù đây là lỗi ở core của wordpress nhưng cách mà phần core wordpress sử dụng lại không trigger được lỗi nên mình đã chuyển sang hướng tìm lỗi trong các plugin và theme. Plugin/theme sẽ gọi đến class WP_Query khi muốn query DB, cách nhận biết lỗi từ source code là khi dùng WP_Query($data) và $data là cái mình có thể control được.
Ví dụ: new WP_Query(json_decode($_POST[‘query_vars’])) thì payload sẽ có dạng:
query_vars={"tax_query":{"0":{"field":"term_taxonomy_id","terms":["<inject>"]}}} hoặcquery_vars={"tax_query":{"0":{"taxonomy":"nav_menu","field":true,"terms":["<inject>"]}}}
Khi dựng môi trường để test lỗi, bật chức năng DEBUG sẽ giúp ta có thể phát hiện SQL Injection qua error-based:
Kết luận
Trong bản vá của wordpress đã thêm phần kiểm tra $query[‘field’] trước, nếu không sẽ convert $query[‘terms’] sang integer nên không thể xảy ra SQLI.
Do số lượng plugin và theme của wordpress khá nhiều, nên team mình mới chỉ tập trung tìm kiếm những cái có lượt download > 100k (bản miễn phí), ngoài ra những plugin/theme bản trả phí hoặc < 100k lượt download thì chúng mình chưa có thời gian để tiếp tục làm.
Kết quả là đã tìm được khá nhiều plugin và theme bị ảnh hưởng bởi lỗ hổng (cả authen và unauthen).
Team mình đã report lỗ hổng này cho ZDI vào cuối tháng 9 và sau 3 tháng lỗi đã được WordPress fix trong core của họ. Cụ thể timeline như sau:
Thanks for reading ^^.
Nguồn: https://cognn.medium.com/sql-injection-in-wordpress-core-zdi-can-15541-a451c492897