Description of Image

Description

The Easy Digital Downloads WordPress Plugin, versions 3.1.0.2 & 3.1.0.3, is affected by an unauthenticated SQL injection vulnerability in the ’s’ parameter of its ’edd_download_search’ action.

Image

Lab-Setup

mkdir cve-2023-23489

Dockerfile

FROM wordpress:latest

# Install development tools and dependencies
RUN apt-get update && apt-get install -y \
    vim \
    subversion \
    git \
    unzip \
    mariadb-client \
    less \
    && rm -rf /var/lib/apt/lists/*

# Install Xdebug for debugging
RUN pecl install xdebug && docker-php-ext-enable xdebug

# Properly configure Xdebug
RUN echo "zend_extension=xdebug" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.remote_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.remote_port=9003" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.log=/tmp/xdebug.log" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini

# Set permissions for WordPress files
RUN chown -R www-data:www-data /var/www/html

# Added phpinfo file
COPY phpinfo.php /var/www/html/phpinfo.php
COPY wp-config.php /var/www/html/wp-config.php

# Set working directory
WORKDIR /var/www/html

Note I got the path of Xdebug Configuration files /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini when the docker container is up i executed this command to get it php --ini Image docker-compose.yml

version: "3" 
# Defines which compose version to use
services:
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: MySQLP@ssw0rd
      MYSQL_DATABASE: WordPressDatabaseName
      MYSQL_USER: WordPressUser
      MYSQL_PASSWORD: P@ssw0rd
    ports:
      - "3306:3306"  # Allow access to MySQL from host (optional)
    volumes:
      - mysql:/var/lib/mysql  # Persist MySQL data

  wordpress:
    depends_on:
      - db
    build: .
    restart: always
    ports:
      - "8000:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: WordPressUser
      WORDPRESS_DB_PASSWORD: P@ssw0rd
      WORDPRESS_DB_NAME: WordPressDatabaseName

volumes:
  mysql:  # Persistent MySQL data

phpinfo.php

<?php phpinfo(); ?>

I updated the wp-config.php file to enable debugging

<?php
/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the installation.
 * You don't have to use the website, you can copy this file to "wp-config.php"
 * and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * Database settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * This has been slightly modified (to read environment variables) for use in Docker.
 *
 * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/
 *
 * @package WordPress
 */

// IMPORTANT: this file needs to stay in-sync with https://github.com/WordPress/WordPress/blob/master/wp-config-sample.php
// (it gets parsed by the upstream wizard in https://github.com/WordPress/WordPress/blob/f27cb65e1ef25d11b535695a660e7282b98eb742/wp-admin/setup-config.php#L356-L392)

// a helper function to lookup "env_FILE", "env", then fallback
if (!function_exists('getenv_docker')) {
	// https://github.com/docker-library/wordpress/issues/588 (WP-CLI will load this file 2x)
	function getenv_docker($env, $default) {
		if ($fileEnv = getenv($env . '_FILE')) {
			return rtrim(file_get_contents($fileEnv), "\r\n");
		}
		else if (($val = getenv($env)) !== false) {
			return $val;
		}
		else {
			return $default;
		}
	}
}

// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', getenv_docker('WORDPRESS_DB_NAME', 'wordpress') );

/** Database username */
define( 'DB_USER', getenv_docker('WORDPRESS_DB_USER', 'example username') );

/** Database password */
define( 'DB_PASSWORD', getenv_docker('WORDPRESS_DB_PASSWORD', 'example password') );

/**
 * Docker image fallback values above are sourced from the official WordPress installation wizard:
 * https://github.com/WordPress/WordPress/blob/1356f6537220ffdc32b9dad2a6cdbe2d010b7a88/wp-admin/setup-config.php#L224-L238
 * (However, using "example username" and "example password" in your database is strongly discouraged.  Please use strong, random credentials!)
 */

/** Database hostname */
define( 'DB_HOST', getenv_docker('WORDPRESS_DB_HOST', 'mysql') );

/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', getenv_docker('WORDPRESS_DB_CHARSET', 'utf8') );

/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', getenv_docker('WORDPRESS_DB_COLLATE', '') );

/**#@+
 * Authentication unique keys and salts.
 *
 * Change these to different unique phrases! You can generate these using
 * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
 *
 * You can change these at any point in time to invalidate all existing cookies.
 * This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
define( 'AUTH_KEY',         getenv_docker('WORDPRESS_AUTH_KEY',         'f9b373ffa89b6310ea66306e2e256351c2293518') );
define( 'SECURE_AUTH_KEY',  getenv_docker('WORDPRESS_SECURE_AUTH_KEY',  'e6b0cac823d4270c0d4240fecaa167579a2a77aa') );
define( 'LOGGED_IN_KEY',    getenv_docker('WORDPRESS_LOGGED_IN_KEY',    'c0c276e7ab64e57ad8457844453a354083e271f4') );
define( 'NONCE_KEY',        getenv_docker('WORDPRESS_NONCE_KEY',        '786d6c04dea080c461b9510402c7231f5ae8e4f1') );
define( 'AUTH_SALT',        getenv_docker('WORDPRESS_AUTH_SALT',        '88e8eb4fe52c0289e6970de0082b58f7ec9fd3af') );
define( 'SECURE_AUTH_SALT', getenv_docker('WORDPRESS_SECURE_AUTH_SALT', '8dfeb7b37d302374d2a11b2f7267cd17013132d1') );
define( 'LOGGED_IN_SALT',   getenv_docker('WORDPRESS_LOGGED_IN_SALT',   '8585878381aa816109b8d23b85061b7c6e3b3740') );
define( 'NONCE_SALT',       getenv_docker('WORDPRESS_NONCE_SALT',       '570b9d081014b1380d940d93501c048aaaa35f5e') );
// (See also https://wordpress.stackexchange.com/a/152905/199287)

/**#@-*/

/**
 * WordPress database table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 *
 * At the installation time, database tables are created with the specified prefix.
 * Changing this value after WordPress is installed will make your site think
 * it has not been installed.
 *
 * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#table-prefix
 */
$table_prefix = getenv_docker('WORDPRESS_TABLE_PREFIX', 'wp_');

/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the documentation.
 *
 * @link https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/
 */
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', '/var/www/html/wp-content/debug.log');
define('WP_DEBUG_DISPLAY', false);
@ini_set('log_errors', 1);
@ini_set('display_errors', 0);


/* Add any custom values between this line and the "stop editing" line. */

// If we're behind a proxy server and using HTTPS, we need to alert WordPress of that fact
// see also https://wordpress.org/support/article/administration-over-ssl/#using-a-reverse-proxy
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
	$_SERVER['HTTPS'] = 'on';
}
// (we include this by default because reverse proxying is extremely common in container environments)

if ($configExtra = getenv_docker('WORDPRESS_CONFIG_EXTRA', '')) {
	eval($configExtra);
}

/* That's all, stop editing! Happy publishing. */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
	define( 'ABSPATH', __DIR__ . '/' );
}

/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';

This are all the created files Image Install Xdebug

sudo apt install php-xdebug -y

Find Xdebug Configuration files

locate xdebug.ini

Image

Xdebug Configurations

zend_extension = xdebug.so
xdebug.mode = debug
xdebug.start_with_request = trigger
xdebug.client_port = 9003
xdebug.client_host = host.docker.internal
xdebug.log = /tmp/xdebug.log

Image

We will use Visual Studio Code for Debugging the Vulnerability

Install Visual Studio Code

After Installation go to Visual Studio Code Extensions Image

Install these Extensions Image

To build the Docker Container docker-compose up Image

To see all running containers Image

When you go to http://localhost:8000/ you will see the wordpress installation page Image
Image
Image
Image I searched for easy digital downloads Image
Image Go to Advanced View Section Image The Description of the vulnerability says that version 3.1.0.3 is vulnerable we will download both 3.1.0.3 & 3.1.0.4 to see the patch of the vulnerability Image Image First lets upload the vulnerable plugin to wordpress press Browse button and choose easy-digital-downloads.3.1.0.3.zip Image Press Install Now Button Image You will get this error, we need to change the max file size Image Open a shell on docker container and execute these commands

echo "upload_max_filesize = 50M
post_max_size = 100M
max_execution_time = 300
memory_limit = 256M" > /usr/local/etc/php/conf.d/custom.ini

Image Then restart Docker container Image If we try to install the plugin again Image It will be uploaded successfully, Press the Activate Plugin button Image The plugin is installed successfully Image

Open Visual Studio Code and press Remote Explorer then choose Attackin Current Window Image Then go to Extensions and Install PHP Debug on the container will be needed for Xdebug Image Choose Open Folder from File Image This bar will appear Image we need to open the Apache directory where WordPress files are installed which is under /var/www/html Image The content of the container appears Image

Lets test if the debugger is working by debugging first file phpinfo.php
Go to Run - Start Debugging Image Then press on create a launch.json file Image launch.json file will show Image I updated the launch.json file to work on the current directory that we opened /var/www/html
Updated launch.json file :-

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [

        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "stopOnEntry": false,
            "pathMappings": {"/var/www/html": "${workspaceFolder}"}
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 0,
            "runtimeArgs": [
                "-dxdebug.start_with_request=yes"
            ],
            "env": {
                "XDEBUG_MODE": "debug,develop",
                "XDEBUG_CONFIG": "client_port=${port}"
            }
        },
        {
            "name": "Launch Built-in web server",
            "type": "php",
            "request": "launch",
            "runtimeArgs": [
                "-dxdebug.mode=debug",
                "-dxdebug.start_with_request=yes",
                "-S",
                "localhost:0"
            ],
            "program": "",
            "cwd": "${workspaceRoot}",
            "port": 9003,
            "serverReadyAction": {
                "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
                "uriFormat": "http://localhost:%s",
                "action": "openExternally"
            }
        }
    ]
}

Then Go to Explorer Image open phpinfo.php file and set breakpoint Image Go to Run - Start Debugging Image You will see the debugging bar appears Image when you visit http://localhost:8000/phpinfo.php/ at browser You will see that the breakpoint is triggered Image

Let’s begin the analysis

unzip easy-digital-downloads.3.1.0.3.zip -d easy-digital-downloads.3.1.0.3
unzip easy-digital-downloads.3.1.0.4.zip -d easy-digital-downloads.3.1.0.4

We will use meld to compare between the original file with the patched one to install Meld sudo apt install meld
From the vulnerability description the vulnerable action is edd_download_search
We will search all PHP files that contains this action edd_download_search

grep -r "edd_download_search" . |  grep '\.php'

Image The ajax-functions.php is the file that contains the action edd_download_search

run meld command

meld

Press File Tab Image

Press here to choose the first file easy-digital-downloads.3.1.0.3/easy-digital-downloads/includes/ajax-functions.php Image Press here to choose the second file /easy-digital-downloads.3.1.0.4/easy-digital-downloads/includes/ajax-functions.php Image The press Compare Button Image Check this box to show line numbers of the code Image As you can see the variable $new_search in the patched version is sanitized Image

If you open the code in the vulnerable version, you will see the variable $new_search is passed with GET Method of s parameter Image

The variable $new_search is under function edd_ajax_download_search()
If you searched through the code for edd_ajax_download_search

Image According to WordPress this is the format of AJAX Actions do_action("wp_ajax_{$action}")

So:-

  • wp_ajax_edd_download_search → Runs edd_ajax_download_search() for logged-in users.
  • wp_ajax_nopriv_edd_download_search → Runs edd_ajax_download_search() for non-logged-in users. so if we want to get edd_ajax_download_search action it will be edd_download_search so the url should be like that
http://localhost:8000/wp-admin/admin-ajax.php?action=edd_download_search&s={user-input}

We removed the ajax keyword from edd_ajax_download_search because WordPress automatically maps AJAX requests using admin-ajax.php based on add_action() hooks.This is the default entry point for AJAX requests in WordPress.

Let’s continue analysis if you search for the variable $new_search inside the code, you will find it’s passed to an array variable $args Image Let’s set a breakpoint at this variable and make the request to ensure it is valid Image

If you go to the URL

http://localhost:8000/wp-admin/admin-ajax.php?action=edd_download_search&s=5555

You will find that the variable $new_search value changed to 5555 Image

If you search for variable $args inside the function edd_ajax_download_search, you will see it’s passed to get_posts Image

Let’s search the files to see where is the get_posts function is located.
You can get it either from command line of a container shell

grep -r "function get_posts()" . | grep '\.php'

Image Or from Visual Studio Code, go to the search icon and type function get_posts(). You will see it’s in the file class-wp-query.php Image

The function get_posts() is wordpress function used in the class WP_Query

We will add this code to functions.php to see the full SQL query

add_filter('posts_request', function($sql, $query) {
    error_log("SQL Query: " . $sql);
    return $sql;
}, 10, 2);

Image As you can see, the active theme is Twenty Twenty-Five theme Image so well edit the functions.php file /wp-content/themes/twentytwentyfive/functions.php Image If you go to the url

http://localhost:8000/wp-admin/admin-ajax.php?action=edd_download_search&s=5555

A file called debug.log will be generated under wp-content, containing the full SQL query. Image As you can see, the user input isn’t sanitized, making it vulnerable to SQL injection.

http://localhost:8000/wp-admin/admin-ajax.php?action=edd_download_search&s=5555%27+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))a)--+-

We test this time-based SQLi query. Image

References

https://nvd.nist.gov/vuln/detail/CVE-2023-23489/ https://wpscan.com/vulnerability/c5a6830c-6420-42fc-b20c-8e20224d6f18/