Intro
Nhân kỷ niệm Wordpress tròn 20 tuổi (thực ra mình cũng không biết ngày này 🤣) thì nay mình có 1 (có thể là 2 hoặc 3 ) bài viết nho nhỏ nói về khía cạnh bảo mật của Wordpress, đó là các plugins. Đây có thể coi là phần tiếp nối và đúc kết những gì mình có chia sẻ tại Virtual Trà Đá Hacking #9 (vẫn chưa thấy chương trình trở lại với mùa mới 😢 - slide ở đây nha) và cả quá trình mình audit source code Wordpress plugins từ trước đến giờ. Let's go!
Trước hết chúng ta cần hiểu qua một chút cơ bản về cách mà Wordpress plugin hoạt động
Wordpress Plugin 101
Hooking
Wordpress Plugin có thể coi là thành phần mở rộng các tính năng dành cho Wordpress. Hiện nay đã có hơn 58,000+ plugin ở tại https://plugins.svn.wordpress.org/ . Và có thể nói là hiếm có một website chạy Wordpress nào mà lại không cài ít nhất một plugin, và người ta vẫn nói đùa với nhau là: "Luôn có một plugin cho bất kỳ một tính năng nào" .
Một plugin về cơ bản vẫn là một thư mục chứa các file code PHP cùng với một file entry point, là nơi mà Wordpress sẽ gọi đến để load các chức năng của plugin vào:
Để có thể mở rộng các tính năng thì các plugin này có thể nhúng các hành động của mình vào từng thời điểm cụ thể trong life cycle của Wordpress thông qua cơ chế: Hooking. Wordpress đã định nghĩa hơn 1900 các hook khác nhau ví dụ như:
- publishpost: thời điểm mà bài viết được công khai.
- savepost: thời điểm mà người dùng lưu lại bài viết.
- aftersignupuser: thời điểm sau khi người dùng đăng ký. ... vân vân và mây mây. Các plugin cũng có thể tự đăng ký các hook và sau đó trigger để sử dụng. Ví dụ với đoạn code như sau:
fuction wpdemo_my_save_post(){
// do something with saved post
} add_action( 'save_post', 'wpdemo_my_save_post');
thì plugin đã định nghĩa một hàm sẽ được thực thi ngay khi người dùng lưu lại bài viết. Khi đó, plugin có thể thêm vào các chức năng của mình trước khi bài viết được lưu vào DB. Ngoài add_action
thì có một kiểu hook khác dùng để thay đổi dữ liệu:
add_filter(the_title, 'make_title_red', 999);
Ngoài ra, plugin có thể trigger trực tiếp các action này thông qua việc gọi hàm do_action
:
hoặc cụ thể hơn như theo document:
// The action callback function.
function example_callback( $arg1, $arg2 ) { // (maybe) do something with the args.
}
add_action( 'example_action', 'example_callback', 10, 2 ); /* * Trigger the actions by calling the 'example_callback()' function * that's hooked onto `example_action` above. * * - 'example_action' is the action hook. * - $arg1 and $arg2 are the additional arguments passed to the callback.
do_action( 'example_action', $arg1, $arg2 );
và một điểm cần lưu ý là:
Tất các hook này đều không thực hiện việc kiểm tra quyền của user đã gọi hàm, do đó nếu thiếu cơ chế kiểm tra và quản lý quyền truy cập sẽ dễ dàng dẫn đến lỗ hổng Broken Access Control (BAC).
Và theo như biểu đồ sau thì lỗ hổng BAC đóng góp một phần tỉ lệ không nhỏ (top 2 chỉ sau XSS) trong tổng số các bug đã được thống kê:
DB Query
Wordpress sử dụng một biến global là $wpdb
(class wpdb) dùng cho các tác vụ truy vấn DB và có thể nói là 99.9999 % các plugin sẽ sử dụng các hàm của class này, theo cách an toàn hoặc không an toàn (here we go SQLi )
(not this code)
REST API
Nếu bạn không biết thì Wordpress đã cung cấp sẵn RESTful API ở tại /wp-json/
. Tuy nhiên để bật tính năng này thì cần đổi Permalink Settings
từ Plain thành kiểu khác, ví dụ như là Post name
)
và các plugin cũng có thể đăng ký các API của riêng mình thông qua hàm register_rest_route
.
và chúng ta có thể tìm thấy tất cả các route đã được đăng ký tại /wp-json/
(cả mặc định của wordpress và custom của plugins) như sau:
Wordpress yêu cầu khi đăng ký các route này, chúng ta cần thêm 1 tham số là permission_callback
, chỉ đến một hàm dùng để kiểm tra quyền truy cập của người dùng (như ví dụ trên, chỉ những người dùng có quyền sửa bài viết mới được phép truy cập). Tuy nhiên, đôi khi các plugin cũng mắc sai lầm trong việc thiết lập các quyền này, ví dụ như set giá trị cho tham số này là __return_true
cho phép REST API này public cho tất cả người sử dụng.
Common Vulnerabilities
Broken Access Control (BAC)
Trong số những hook ở trên, có một số hook rất đặc biệt, và những hook này thường là nơi dễ xuất hiện lỗ hổng BAC nếu không được kiểm tra quyền một các đầy đủ. Chúng ta cùng xem xét qua một số của chúng:
admin_init
: hook này dùng để chạy một hàm khi chúng ta load vào màn hình quản lý của Wordpress. Mặc dù có chữadmin
bên trong tên nhưng hook này hoàn toàn không kiểm tra được nó có gọi đến bởi admin hay không. Các hàm đănng ký với hook này sẽ được thực thi khi người dùng truy cập vào fileadmin-ajax
hoặcadmin-post.php
. Mà hai file này hoàn toàn có thể được gọi đến bởi một người dùng chưa xác thực dẫn tới lỗ hổng nếu tồn tại ở vị trí này sẽ cực kỳ nguy hiểm.admin_menu
: hook này sẽ được thực thi khi người dùng truy cập vào giao diện ở/wp-admin/index.php
. Nó thường được dùng là nơi plugin init các menu hoặc các thành phần giao diện, các tab trong phần setting.
- Nó yêu cầu người dùng đã xác thực, ít nhất phải có role là Subscriber. Wordpress đã định nghĩa sẵn 1 số quyền cơ bản:
- Subscriber: người theo dõi, chỉ có quyền đăng nhập vào chỉnh sửa profile của mình.
- Contributor: người có thể viết bài (nhưng không upload được media) và bài viết phải được review và approve bởi người dùng có quyền cao hơn mới được publish.
- Author, Editor: người dùng có quyền viết bài và đôi khi là quản lý một số settings của các plugin. Không có quyền tạo hoặc chỉnh sửa user.
- Administrator: người có quyền quản trị toàn bộ trang.
Nếu có lỗ hổng BAC tại hook này, người dùng có quyền thấp hoàn toàn có thể nâng quyền của mình lên mức cao hơn, và đôi khi là take-over toàn bộ site.
admin_post
: thực thi khi người dùng POST vào/wp-admin/admin-post.php
. Ngoài ra còn có một hàm tương tự làadmin_post_nopriv
dành cho các action không cần quyền. Tuy nhiên, cả 2 hook này như đã nó ở trên thì đều không check quyền của người dùng nên cần được implement ở trong hàm được gọi.wp_ajax
: thực thi khi người dùng truy cập (POST
hoặcGET
) vào/wp-admin/admin-ajax.php
, Ví dụ như đoạn code sau:
thì với định nghĩa
add_action( 'wp_ajax_cnb_delete_action', array( $action_controller, 'delete_ajax' ) );
thì chúng ta cần truyền một tham số action
có giá trị là cnb_delete_action
( phần sau ở wp_ajax_XXX
) là sẽ gọi được đến hàm delete_ajax
.
Một hook khác tương tự là wp_ajax_no_priv
cũng có cách hoạt động tương tự. Và nhiều khi developer nhầm rằng chỉ có admin mới có thể gọi các hàm này.
admin_action
: hook được gọi khi người dùng thực thi các hành động trong màn hình quản lý, và tất nhiên, không thực hiện kiểm tra quyền của người dùng.- Ngoài ra còn một số hook khác như
profile_update
vàpersonal_options_update
liên quan đến cập nhật profile của người. Kẻ tấn công có thể lợi dụng lỗ hổng BAC ở các hook này để nâng quyền lên thành người dùng có quyền cao hơn.
Cùng xem thử một ví dụ để hiểu sâu hơn nhé.
CVE-2023-30869 Easy Digital Downloads Plugin 3.1 - 3.1.1.4.1 - Unauthenticated Privilege Escalation Vulnerability
On April 21st, 2023, Nguyen Anh Tien reported a critical vulnerability to us that exists in the Easy Digital Downloads plugin versions 3.1.1.4.1 and below. This vulnerability makes it possible for any user, regardless of their current authentication and authorization, to execute any action registered with the prefix edd_.
Khi nhìn vào đoạn code dưới đây chúng ta có thể thấy plugin đã đăng ký một hàm edd_validate_password_reset
tương ứng với hook edd_user_reset_password
. Và ở trong hàm này ta có thể thấy rõ ràng là không có một đoạn nào kiểm tra xem người dùng đã gọi hàm này là ai, và sau đó thực hiện cập nhật password (thông qua hàm reset_password
) của người dùng dựa trên tham số user_login
(tức là ta chỉ cần biết tên đăng nhập của người đó là đủ. Thông tin này hoàn toàn có thể lấy được thông qua RESTful API của wordpress ở URL: http://example.com/wp-json/wp/v2/users/1 ). Việc code riêng một chức năng reser password mà thông theo flow chung của wordpress (thông qua hàm check_password_reset_key
) là cực kỳ nguy hiểm.
Tuy nhiên, hàm này được gọi như thế nào? Chúng ta mới chỉ có add_action
mà chưa có do_action
. Chúng ta có thể tìm thấy logic này ở easy-digital-downloads\includes\actions.php
Plugin đã định nghĩa một hàm để gọi các action tương ứng theo tham số edd_action
được truyền vào qua $_GET
và $_POST
. Các hàm edd_get_actions
và edd_post_actions
sẽ được kết nối vào hook init
và theo định nghĩa của hook init
ở dưới đây:
thì nó được gọi ngay khi Wordpress load xong. Đến đây chúng ta chỉ cần truyền vào &edd_action=validate_password_reset
ở admin-ajax.php
là đã có lỗi auth leo quyền thành bất cứ người dùng nào, hoặc đơn giản hơn là take-over toàn bộ wordpress website.
Ở đây có một trick nho nhỏ giúp ta có thể biến lỗi auth này thành unauth. Ở admin-ajax.php
có một hook có thể được gọi bởi cả người dùng đã xác thực và chưa xác thực. Đó là heartbeat
. Và chúng ta cần truyền tham số như sau: &action=heartbeat
. PoC hoàn chỉnh để đổi password của user admin
thành a
như sau:
POST /wp-admin/admin-ajax.php?edd-action=user_reset_password&pass1=a&pass2=a&user_login=admin HTTP/1.1
Host: localhost:7080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 16
Origin: http://localhost:7080
DNT: 1
Connection: close
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin action=heartbeat
Cùng xem plugin đã fix buy này như thế nào:
- Action này hoàn toàn không được gọi qua AJAX nên báo lỗi ngay khi phát hiện nó được gọi qua
admin-ajax.php
. - Thực hiện đúng quy trình reset password của wordpress thông qua hàm
check_password_reset_key
, kiểm tra token tương ứng với user.
Tuy nhiên với kiểu code catch all như thế kia thì mình nghi ngờ rằng ngoài hàm này ra vẫn còn các hàm khác có thể khai thác lỗ hổng BAC ở plugin này. Các bạn tìm thử xem sao nha
Một lỗi khác tương tự có thể tham khảo thêm: https://patchstack.com/articles/critical-privilege-escalation-in-essential-addons-for-elementor-plugin-affecting-1-million-sites/
To be continue...
Chúng ta đã đi qua phần kiến thức cơ bản và một class lỗ hổng có thể coi là phổ biến nhất trong các wordpress plugins. Nếu bạn đã nắm chắc được cách khai thác thì mình đảm bảo bạn sẽ tìm thấy không ít lỗ hổng rồi đó . Ở phần tới chúng ta sẽ cùng xem xét các loại lỗ hổng khác nha. Till next time ~
References
- https://developer.wordpress.org/reference/hooks/
- https://developer.wordpress.org/?s=init
- https://patchstack.com/articles/critical-privilege-escalation-in-essential-addons-for-elementor-plugin-affecting-1-million-sites/
- https://patchstack.com/articles/critical-easy-digital-downloads-vulnerability/
- https://docs.google.com/presentation/d/1G4g1IqNo0uz89Yzw-8FPyPHZtkqfImuNbNHO8qB_LoY/edit#slide=id.g9cbd232d60_0_7
- https://plugins.trac.wordpress.org/