WordPress CTF Event Writeup

WP Elevator

โ€”

by

Summary

This was my first CTF and 1 of the challenges provided by Patchstack as part of their September WCUS CTF.

Challenge

The Challenge was to find a flag (a text file) in a WordPress Website. We begin with no backend authorisation, just a the following information:

Asked my freelance developer friend to write me an authorization plugin so I can share knowledge with selected members. He is still working on it but gave me an early version. I don’t know how it works but will talk with him once he finishes.

Note: fully whitebox challenge, no need to do massive bruteforce

Along with this description, we were provided with a .zip file which contained the WordPress Plugin which is installed on the target site.

The target site itself appeared to be a fresh Twenty-Twenty-Four WordPress installation with registration by default disabled.

Recon

The WordPress Plugin

on inspection, the code contained a plugin called P-member-manager. There weren’t many files which needed inspecting, and I could see straight away the root file p-member-manager.php looked to contain some leaks. As I moved towards the end of the plugin file, I found a useful looking function:

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.");
    }
}
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,
    ];
}

Well that was easy, we found the flag! This function flagger_request_callback will allow us to retrieve the contents of our flag.txt file as a JSON value. But before we start celebrating, in order to get the value we will need to hold either an administrator or subscriber role with an associated (and valid) nonce.

This makes our first priority to be gaining some sort of access to the site necessary to escalate to either an administrator or subscriber role before moving to the next step.

Creating an account

adadd_action("rest_api_init", "register_user_creation_endpoint");

function register_user_creation_endpoint()
{
    register_rest_route("user/v1", "/create", [
        "methods" => "POST",
        "callback" => "create_user_via_api",
        "permission_callback" => "__return_true", // Allow anyone to access this endpoint
    ]);
}

add_action("wp_ajax_reset_key", "reset_password_key_callback");
add_action("wp_ajax_nopriv_reset_key", "reset_password_key_callback");

The plugin registers an endpoint at the route /wp-json/json/user/v1/create. a simple post request gives us a subscriber account. There is no authentication required, so we can go ahead and register a user.

Post Request to the vulnerable WordPress endpoint to create the user

At this point we had registered ourselves on the site, but this particular instance had email disabled, meaning we couldn’t just reset our password in the usual WordPress Forgot password manner.

Resetting the User Password

There was a function get_password_reset_key2 which was generating a new password reset key. Brute force usually isn’t my style, but as you can see the generated key was set at a 1 character string only.

Since it’s set to false, the combination is only one of the following:

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789

That presents us a total characters of either 26 (lowercase) + 26 (uppercase) + 10 (digits) = 62 characters

// Generate something random for a password reset key.
    $key = wp_generate_password(1, false);
    /**
     * Fires when a password reset key is generated.
     *
     * @since 2.5.0
     *
     * @param string $user_login The username for the user.
     * @param string $key        The generated password reset key.
     */
    do_action("retrieve_password_key", $user->user_login, $key);

After iterating through the 62 possible characters and resetting our password, we are logged in as a subscriber.

Obtaining the WordPress Nonce

Now we just need a nonce to get access to reading the flag.txt. Here we are able to post to the get_latest_posts action which will give us the nonce which we need to read our flag.txt. In order to do so, we have to prove that we are a Subscriber, and obtain the session cookie via Network > Dev Tools.

add_action("wp_ajax_get_latest_posts", "get_latest_posts_callback");

function get_latest_posts_callback()
{
    // Check if the current user has the subscriber role
    if (!current_user_can("subscriber")) {
        wp_send_json_error("Unauthorized access.");
        return;
    }

    // Generate nonce
    $nonce = wp_create_nonce("get_latest_posts_nonce");

    // Get latest 5 posts
    $args = [
        "posts_per_page" => 5,
        "post_status" => "publish",
        "orderby" => "date",
        "order" => "DESC",
    ];

    $latest_posts = get_posts($args);

    // Prepare posts data
    $posts_data = [];
    foreach ($latest_posts as $post) {
        $posts_data[] = [
            "title" => $post->post_title,
            "content" => $post->post_content,
            "link" => get_permalink($post),
        ];
    }

    // Send response with nonce and posts data
    wp_send_json_success(["nonce" => $nonce, "posts" => $posts_data]);
}
post request to get the token CTF

We add the Key: Action Value: Get_latest_posts and the success response will provide our nonce.

Reading The Flag

We have one thing left to do, remember that function at the start? Well lets send a POST request with the recently acquired nonce and see what we get.

Sending the POST request to get the WordPress CTF


We got the flag! Unfortunately I rushed to send it through that I forgot to save it. So let’s just pretend it was CTF{I_fOrGot_tO_saVE_it}

Summary

My first WordPress CTF (and CTF in general) was fun but tough. I over complicated things, and need to spend more time carefully auditing the code. Ironically, I also tried a second task called ‘Link Manager’ and spent far more time on it getting very close and gaining access to the site, but not being able to retrieve the flag. All in all, I will continue to do more CTF’s and will post my reviews on the site afterwards.

Patchstack Capture The Flag WCUS Dashboard