Giới thiệu
WordPress là một hệ thống quản lý nội dung (CMS) phổ biến trên thế giới, được sử dụng rộng rãi cho việc xây dựng và quản lý các trang web và blog. Với giao diện người dùng dễ sử dụng và một cộng đồng đông đảo, WordPress đã trở thành một lựa chọn phổ biến cho cả người mới bắt đầu và những người có kinh nghiệm trong việc tạo nội dung trực tuyến.
Một trong những lợi ích lớn nhất của WordPress là khả năng mở rộng thông qua các plugin. Các plugin là những module mở rộng chức năng của WordPress, giúp người dùng thêm các tính năng và quyền lợi mà hệ thống cơ bản không có. Có hàng nghìn plugin có sẵn, từ những plugin nhỏ để thêm các tính năng như biểu tượng chia sẻ xã hội, đến những plugin lớn mở rộng khả năng quản lý nội dung, thương mại điện tử, và SEO.
Đi kèm với số lượng plugin lớn như vậy, là khả năng mất an toàn cao khi nhiều developer của các plugin này chưa biết áp dụng secure coding vào code của mình dẫn tới plugin có nguy cơ chứa các lỗ hổng nguy hiểm mà phổ biến là Cross site scripting (XSS), SQL injection...
Và vâng, đó cũng là nguyên nhân chính dẫn đến bài viết ngày hôm nay. Mình sẽ chia sẻ các bước mình thực hiện audit source code để tìm các lỗ hổng trong wordpress plugin và một số điều thú vị có thể bạn chưa biết !!!!
Chuẩn bị môi trường
Để mà tìm bug trên wordpress plugin thì có 2 thứ mà chúng ta không thể thiếu. Hiển nhiên đó là 1 con server chạy Wordpress và các plugin đi kèm hay nói đúng hơn là source code của những plugin đó.
Wordpress
Cách cài đặt thằng wordpress này thì rất đơn giản rồi, trên google đầy rẫy những bài hướng dẫn cài đặt nó, tùy vào hệ điều hành mà bạn sử dụng như Linux hay Windows thì sẽ có cách cài đặt khác nhau. Mình thì sử dụng xampp và cài wordpress local trên xampp. Phần này dễ rồi nên tạm skip qua nhảy xuống bước dưới khó hơn này.
Wordpress plugin
Để cài đặt một plugin trên wordpress thì cũng không có gì để nói. Bạn chỉ cần đăng nhập vào admin và click install bất kì plugin nào bạn muốn ở menu Plugins. Vấn đề ở đây bắt đầu xảy ra. Mục tiêu của bạn là đi tìm bug trong wordpress plugin kia mà. Bạn không thể nào click install từng thằng một, chưa kể bạn cũng chẳng biết trong cái plugin bạn vừa install liệu có bug hay không nữa .
Phải có cách nào đó thông minh hơn, đỡ thủ công hơn và ít nhất với suy nghĩ khá khẩm hơn một chút đó là, càng nhiều source code plugin mình có, khả năng tìm được lỗi sẽ càng cao hơn. Và rồi nó xuất hiện.
Mình tìm được 1 api của wordpress cho biết được toàn bộ thông tin của plugin hiện có trên wordpress: https://api.wordpress.org/plugins/info/1.2/?action=query_plugins
.
Như bạn có thấy, nó chứa cả số lượng active install lẫn download link của plugin theo định dạng zip. Điều này rất phù hợp nếu bạn chỉ muốn download những plugin có số lượng active install nhất định. Công việc còn lại là viết script download hàng loạt plugin xuống và chúng ta đã có một code base vô cùng lớn để tìm bug rồi (chi tiết script như thế nào bạn đọc tự viết nhé).
Tool Scan || Manual
Khi đã có source code rồi thi bắt tay vào tìm bug thôi. Ơ mà khoan, tìm như nào. Bạn hoàn toàn có thể sử dụng regex để tìm kiếm với những pattern đơn giản. Ví dụ mình muốn tìm kiếm những đoạn code có biến $_REQUEST
hay $_GET
xuất hiện trong hàm echo
để tìm lỗi XSS chẳng hạn thì mình sẽ sử dụng regex
echo\s+.*(\$_REQUEST\['[^']+'\]|\$_GET\['[^']+'\]).*
Kết quả đưa ra cũng khá khả thi
Tuy nhiên, việc này sẽ làm chúng ta sót rất nhiều lỗi, do có nhiều trường hợp sử dụng các biến trung gian nhận input của người dùng sau đó mới echo ra. Dó đó các static analysis tool ra đời nhằm giải quyết vấn đề này. Có nhiều công cụ hữu ích đối với ngôn ngữ PHP như progpilot, Sonarqube... Và mình chọn sử dụng Sonarqube.
Có thể thấy được ngay công cụ đã phân tích flow đi từ source đến sink một cách rất rõ ràng. Sonarqube cũng hỗ trợ custom sink source nữa, tuy nhiên cũng không phải dễ dàng gì và có lẽ đó sẽ lại là một bài viết khác của mình vào một thời điểm khác. Nói đi thì cũng phải nói lại, công cụ hữu ích là vậy nhưng tỉ lệ false positive vẫn còn rất rất nhiều. Bạn bắt buộc phải verify lại một cách thủ công.
Tìm và khai thác lỗ hổng
Lỗ hổng XSS
Với case đơn giản nhất này, input của bạn sẽ được echo
ra ngay hoặc được nối chuỗi trong html mà không được lọc bởi bất cứ filter nào thì ta dễ dàng có 1 lỗ hổng reflected XSS ở đây chỉ với payload:
localhost:8085/wordpress/wp-admin/admin.php?page=nggallery-manage-gallery&input=<img+src=1+onerror=alert(1)>
Vậy nếu ta gặp tình huống như đoạn html dưới đây thì phải làm thế nào. Input của chúng ta được reflected vào href trong thẻ a.
<a href="abc<?php echo$_GET['input']; ?>">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</a>
Bạn sẽ suy nghĩ là phải tìm cách để escape được ra khỏi cái action này bằng cách chèn input="
phải không. Tuy nhiên, mặc định thì với các dấu " hay ', wordpress sẽ tự động thêm \
vào trước kí tự đặc biệt này
Như vậy thẻ a lúc này của chúng ta sẽ có dạng
<a href="abc\"">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</a>
Theo lối suy nghĩ thông thường thì dấu \
được dùng để đánh dấu các kí tự đặc biệt và do đó \"
sẽ được hiểu là string bên trong ""
. Nhưng html say no, html chỉ hiểu là khi có dấu "
xuất hiện tức là nơi đó là nơi kết thúc chuỗi và hiển nhiên \
sẽ được hiểu là string như bao kí tự khác mà thôi.
Payload:
localhost:8085/wordpress/wp-admin/admin.php?page=nggallery-manage-gallery&input="><img+src=1+onerror=alert(1)>
Giả sử tôi là developer của plugin này, và tôi đã biết bạn có thể tấn công trang web của tôi thông qua lỗ hổng XSS tại đoạn code này. Tôi bắt đầu tìm kiếm giải pháp và wow, sanitize_text_field
xuất hiện.
Nó có thể lọc các thẻ html và do đó payload ở trên không thể thành công được nữa.
<a href="abc<?php echo sanitize_text_field($_GET['input']); ?>">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</a>
Tuy nhiên thua keo này ta bày keo khác. Lọc html tag không có nghĩa là ta không thể khai thác XSS theo một cách khác. Không biết các bạn đã nghe đến thuộc tính autofocus
và onfocus
hay chưa.
Thuộc tính autofocus
trong HTML được sử dụng để tự động đặt trỏ vào một phần tử trang web ngay khi trang tải xong và onfocus
là một sự kiện trong JavaScript, được kích hoạt khi một phần tử nhận được sự chú ý (focal point). Nếu ta thêm được thuộc tính và sự kiện này vào thẻ thì hiển nhiên ta vẫn có thể tấn công XSS được. Như tôi đã trình bày ở trên, ta hoàn toàn có thể escape chuỗi bằng cách thêm "
, như vậy cách này hoàn toàn có thể thực hiện. Và cuối cùng payload của chúng ta trông như sau:
http://localhost:8085/wordpress/wp-admin/admin.php?page=nggallery-manage-gallery&input="+autofocus+onfocus=alert(1)+x=1
Một case thú vị nữa về XSS mình muốn chia sẻ. Mình cam đoan sẽ có lúc bạn review source code và sẽ gặp trường hợp như đoạn code dưới
<?php function glsr_admin_url($page = '', $tab = '', $sub = ''){ $args = array_filter(compact('page', 'tab')); return add_query_arg($args, admin_url('index.php'));
}
?>
<a href="<?=glsr_admin_url($_GET['input'], 'functions'); ?>">this is a tag</a>
input của user sẽ được truyền vào hàm add_query_arg
sau đó sẽ đưa vào href của thẻ a. Hàm này sẽ thêm các tham số vào url. Ví dụ với đoạn code trên khi input=abc
thì url được build sẽ là http://localhost:8085/wordpress/wp-admin/index.php?page=abc&tab=functions
<a href="http://localhost:8085/wordpress/wp-admin/index.php?page=abc&tab=functions">this is a tag</a>
Đến đây thì nó cũng không khác gì so với case phía trên, payload cũng tương tự. Case này mình nêu ra vì trong sonarqube, công cụ sẽ không thể detect được source là các biến truyền vào hàm add_query_arg
đâu, mà thay vào đó nó sẽ nhận add_query_arg
làm soucrce. Do đó bạn cần phải làm thủ công thêm một số bước để xác nhận xem ở đây có phải là lỗi hay không.
Lỗ hổng SQL injection
Ở loại lỗ hổng này thì công cụ đã làm rất tốt công việc của mình đó là detect sink source và flow từ source đến sink rồi. Tuy nhiên thì trong code cũng bị filter khá nhiều.
$wpdb->get_var("SELECT 'included_rules' FROM {$wpdb->prefix}proofreading_rules_settings WHERE lang_code = '" . $_REQUEST['language'] . "'");
Chẳng hạn như đoạn code trên không bị lỗi sql injection vì ta không thể escape ra khỏi cặp ''
được. Để rõ hơn, nếu ta truyền language='1
thì câu truy vấn sẽ trông như sau:
SELECT 'included_rules' FROM {$wpdb->prefix}proofreading_rules_settings WHERE lang_code = '1\''
Ví dụ dưới đây thì đoạn code sẽ bị lỗi SQL injection vì chúng ta không cần phải escape string và biến $pTC_logging_level
chúng ta có thể kiểm soát được
$pTC_logs_query = "SELECT p.time, p.priority, u.user_login, p.message FROM " . $pTC_logs_table . " AS p, " . $wpdb->users . " AS u WHERE p.userid = u.ID AND p.priority <= " . $pTC_logging_level
Tuy nhiên khi mà kiếm tra, tìm lỗ hổng về SQL injection, bạn cần phải thử xem nó có hoạt động với các role quyền thấp hơn như contributor, subcriber hay không, vì đơn giản nếu chỉ admin mới trigger được thì với mình nó không có ý nghĩa.
Để phòng chống sqli trong plugin thì developer có thể sử dụng esc_sql
và $wpdb->prepare
. $wpdb->prepare()
hoạt động bằng cách lọc (sanitize) và thoát (escape) các giá trị trong truy vấn SQL. Nó thực hiện điều này bằng cách thay thế các placeholder (như %s cho chuỗi, %d cho số nguyên, và %f cho số dấu phẩy động) trong chuỗi truy vấn bằng giá trị thực tế sau khi chúng đã được thoát một cách an toàn. Tuy nhiên, trong khá nhiều trường hợp, dev sử dụng $wpdb->prepare()
chưa triệt để. Tại sao lại nói như vậy, ta hãy cùng xem xét một ví dụ bên dưới:
$query = $wpdb->prepare( "SELECT * FROM $table_name WHERE queue_id = %d and status in (" . $stat . ") order by id limit 0,10", $queue_id );
ứng dụng đã sử dụng $wpdb->prepare
để tham số hóa và ép kiểu $queue_id
về kiểu số nguyên sau đó mới đưa vào truy vấn, do đó ta không thể chèn SQL vào biến này. Tuy nhiên biến $stat
lại được bỏ ngỏ ở đây mà không thực hiện cơ chế phòng chống nào cho nên lỗi vẫn có thể xảy ra khi ta cho $stat=1) and sleep5(
=> SELECT * FROM $table_name WHERE queue_id = %d and status in (1) and sleep(5) order by id limit 0,10
.
Ngoài ra nếu đầu vào của chúng ta bị ép kiểu trước khi đưa vào câu truy vấn chẳng hạn như sử dụng absint
, intval
... thì chúng ta cũng không làm ăn được gì => no bug.
Mình sẽ tạm kết bài viết ở đây, nếu tiếp theo mình tìm thấy gì hay ho mình sẽ chia sẻ với các bạn trong một bài viết khác. Cảm ơn các bạn đã đọc bài.