Add exploitarium archive

This commit is contained in:
ashton
2026-06-23 00:13:35 -05:00
commit b5d099261a
99 changed files with 5715 additions and 0 deletions

3
lunar-modrinth-chain-poc/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
poc/poc-output/
*.log
*.tmp

View File

@@ -0,0 +1,172 @@
# Lunar Client Modrinth Explore Chain PoC
Proof package for a high-severity Lunar Client chain observed in the June 2026
unpacked Electron application.
This repository documents a practical RCE-style chain and includes a benign
cross-platform calc-pop proof for the final "drop local launcher and open it via
the OS shell" primitive.
## Status
High-confidence critical candidate, not yet a fully packaged public
Modrinth-to-Lunar end-to-end exploit.
The chain is severe because it joins several individually dangerous behaviors:
- Modrinth project content is rendered in Lunar Explore as raw HTML.
- The Explore renderer has access to privileged Lunar preload APIs.
- The renderer can reach raw IPC/Redux state synchronization into main.
- Main accepts forged profile state and materializes provider profiles.
- Modrinth override extraction can write root-level override files into a
profile-controlled game directory.
- The unverified modpack warning path only scans `mods`, `resourcepacks`, and
`shaderpacks`, not root overrides.
- `openExternalLink` can reach `shell.openExternal` for non-HTTP URLs when the
initiator is not one of the two restricted user-input initiators.
- Opening shell-dispatched local launcher files can execute code. On Windows,
the reviewed chain uses `.lnk`; the included proof also has macOS/Linux
branches for environments where Windows is unavailable.
If the live Modrinth delivery leg is confirmed end-to-end through Lunar's
production cache, the likely impact is arbitrary code execution as the victim's
desktop user after the victim views or clicks a malicious Modrinth project in
Lunar Explore. This path does not require launching Minecraft, having a JRE
ready, or having a selected Minecraft account.
## Impact
Estimated severity: critical.
Tentative CVSS v3.1: `AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H` = 9.6.
This score assumes the attacker can publish or otherwise get attacker-controlled
Modrinth project/changelog content rendered by Lunar Explore, and that the raw
HTML delivery path executes script-capable content in the packaged renderer.
## Chain Summary
1. Attacker-controlled Modrinth Markdown is fetched by Lunar Explore.
2. Lunar renders that Markdown through `ReactMarkdown` with `rehypeRaw`, without
an observed sanitizer or HTML allowlist.
3. A raw HTML payload can execute renderer JavaScript through an iframe-style
delivery primitive.
4. Renderer JavaScript can use exposed preload APIs and raw IPC/Redux sync.
5. The renderer forges or creates a Modrinth provider profile whose
`overrides.gameDirectory` points at a writable attacker-chosen directory.
6. The renderer asks main to install the forged Modrinth profile.
7. Main downloads the `.mrpack`, saves the profile, and extracts root-level
`overrides/*` entries to `getEffectiveGameDirectory(profile)`.
8. A root override such as a benign launcher file is written to the chosen
directory and is not covered by the unverified-file warning scanner.
9. The renderer calls the external-link API on a `file:///.../<launcher>` URL
using a non-restricted initiator.
10. Main reaches `shell.openExternal(url)`. The operating system dispatches the
local launcher.
## Evidence From Local Review
The following paths refer to extracted source-map sources from the reviewed
Lunar Client build.
- Raw HTML Markdown sink:
`src/renderer/impl/app/pages/explore/project/components/markdown.tsx`
imports `ReactMarkdown`, uses `rehypeRaw`, and does not apply a sanitizer.
- Modrinth content flow:
`src/renderer/impl/app/pages/explore/project/utils/fetch.ts` maps project
`body` and version `changelog` into renderer state.
- Main install handler:
`src/electron/module/modrinth/index.ts` exposes `installModpack`.
- Profile source of truth:
`src/electron/module/modrinth/install/modpack/index.ts` reads the target
profile from `launcherRedux.store.getState().profiles.profiles`.
- Profile reducer:
`src/shared/store/profiles/index.ts` accepts `profiles/addOrUpdateProfile`
and inserts or replaces the supplied profile object.
- Profile persistence:
`src/electron/module/profiles/index.ts` preserves `profile.overrides` when
saving a virtual profile.
- Effective game directory:
`src/electron/module/profiles/paths.ts` returns
`profile.overrides.gameDirectory` when present.
- Override extraction:
`src/electron/module/profiles/handlers/extract-overrides/utils.ts` maps
non-content-dir `overrides/*` entries to the effective game directory.
- Warning coverage:
`src/electron/module/profiles/handlers/unverified-modpack-files/consts.ts`
scans only `overrides/mods/`, `overrides/resourcepacks/`, and
`overrides/shaderpacks/`.
- External open sink:
`src/electron/window/preload/impl/misc.ts` blocks non-HTTP protocols only for
selected initiators, then calls `shell.openExternal(url)`.
- Redux bridge:
`src/electron/redux/index.ts` enables `stateSyncEnhancer()` with no observed
application-level action allowlist.
## Calc-Pop PoC
Run this only on a local test machine. It does not interact with Lunar Client or
any live Modrinth project. It validates the final execution primitive by:
1. writing a marker file,
2. creating a local platform-appropriate launcher file,
3. asking the OS shell to open that launcher, and
4. popping the Calculator app where available.
```bash
npm run poc
```
Expected output:
```text
marker: calc-pop-attempted
opened: <launcher path>
```
Platform behavior:
- Windows: creates and opens a `.lnk` pointing to `calc.exe`.
- macOS: creates and opens a `.command` launcher that runs Calculator.
- Linux: creates and opens a `.desktop` launcher for the first available
calculator binary from a small allowlist.
In the original audit environment, the Windows shortcut primitive also wrote
`lnk-executed` to a marker file when the shortcut was opened.
## What Is Intentionally Not Included
This repository does not include:
- A live malicious Modrinth project.
- A weaponized iframe or renderer payload.
- A `.mrpack` containing an executable launcher.
- A script that drives Lunar Client against real users.
See `poc/renderer-chain-skeleton.md` for a non-executable outline of the
renderer-side chain.
## Fix Guidance
Recommended fixes should be layered:
- Disable raw HTML in Modrinth Markdown, or sanitize with a strict allowlist.
- Forbid script-capable embedded content in Explore project descriptions and
changelogs.
- Remove generic renderer access to raw IPC `sendMessage`, or enforce a strict
channel allowlist in preload.
- Disable or constrain Electron Redux state sync from untrusted renderers.
- Validate profile objects at every IPC/main boundary.
- Do not accept arbitrary renderer-supplied `gameDirectory` paths without a
real user gesture and path policy.
- Treat every `overrides/*` archive entry as potentially dangerous, including
root-level files.
- Block `file:`, `ms-*`, and other non-web protocols in `openExternalLink`
unless there is a narrow, explicit allowlist.
- Refuse to open executable file types such as `.lnk`, `.exe`, `.bat`, `.cmd`,
`.ps1`, `.vbs`, and similar from renderer-controlled URLs.
## Disclosure Note
This is intended for authorized validation and coordinated disclosure. Keep the
repository private until the vendor has acknowledged and remediated the issue.

View File

@@ -0,0 +1,37 @@
# Local Launcher Proof
Observed during local validation:
```text
Directory: work\lnk-proof
marker.txt
payload.lnk
lnk-executed
```
Interpretation:
- A local `.lnk` was created with a harmless marker target.
- Opening the shortcut caused Windows to execute the target.
- The marker file contained `lnk-executed`.
This validates the final operating-system primitive used by the proposed Lunar
chain. It does not prove the complete Lunar end-to-end exploit by itself.
The repository now includes `poc/calc-pop.js`, a Node.js proof that performs a
visible calculator pop using a local launcher file:
- Windows: `.lnk` to `calc.exe`
- macOS: `.command` running `open -a Calculator`
- Linux: `.desktop` launcher for an installed calculator binary
Observed output from the replacement PoC on Windows:
```text
> lunar-modrinth-chain-poc@0.1.0 poc
> node poc/calc-pop.js
marker: calc-pop-attempted
opened: ...\poc\poc-output\calc-pop.lnk
```

View File

@@ -0,0 +1,10 @@
{
"name": "lunar-modrinth-chain-poc",
"version": "0.1.0",
"private": true,
"description": "Benign calc-pop proof for the Lunar Client Modrinth Explore chain.",
"scripts": {
"poc": "node poc/calc-pop.js"
},
"license": "UNLICENSED"
}

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env node
"use strict";
const { existsSync, mkdirSync, writeFileSync, chmodSync } = require("node:fs");
const { join } = require("node:path");
const { spawn, spawnSync } = require("node:child_process");
const outDir = join(__dirname, "poc-output");
mkdirSync(outDir, { recursive: true });
const markerPath = join(outDir, "marker.txt");
writeFileSync(markerPath, "calc-pop-attempted\n", "utf8");
function detached(command, args, options = {}) {
const child = spawn(command, args, {
detached: true,
stdio: "ignore",
windowsHide: false,
...options,
});
child.unref();
}
function commandExists(command) {
if (process.platform === "win32") {
return spawnSync("where", [command], { stdio: "ignore" }).status === 0;
}
return spawnSync("sh", ["-lc", `command -v ${command}`], {
stdio: "ignore",
}).status === 0;
}
function windowsShortcutProof() {
const shortcutPath = join(outDir, "calc-pop.lnk");
const jscriptPath = join(outDir, "create-shortcut.js");
const escapedShortcut = shortcutPath.replace(/\\/g, "\\\\");
writeFileSync(
jscriptPath,
[
'var shell = WScript.CreateObject("WScript.Shell");',
`var shortcut = shell.CreateShortcut("${escapedShortcut}");`,
'shortcut.TargetPath = "calc.exe";',
'shortcut.WindowStyle = 1;',
"shortcut.Save();",
"",
].join("\r\n"),
"utf8"
);
const created = spawnSync("cscript.exe", ["//nologo", jscriptPath], {
stdio: "inherit",
windowsHide: true,
});
if (created.status !== 0 || !existsSync(shortcutPath)) {
throw new Error("Failed to create Windows shortcut proof");
}
detached("cmd.exe", ["/c", "start", "", shortcutPath]);
return shortcutPath;
}
function macLauncherProof() {
const launcherPath = join(outDir, "calc-pop.command");
writeFileSync(launcherPath, "#!/bin/sh\nopen -a Calculator\n", "utf8");
chmodSync(launcherPath, 0o755);
detached("open", [launcherPath]);
return launcherPath;
}
function linuxLauncherProof() {
const calculators = [
"gnome-calculator",
"kcalc",
"qalculate-gtk",
"mate-calc",
"galculator",
"xcalc",
];
const calculator = calculators.find(commandExists);
if (!calculator) {
throw new Error(
`No supported calculator found. Tried: ${calculators.join(", ")}`
);
}
const launcherPath = join(outDir, "calc-pop.desktop");
writeFileSync(
launcherPath,
[
"[Desktop Entry]",
"Type=Application",
"Name=Calc Pop Proof",
`Exec=${calculator}`,
"Terminal=false",
"",
].join("\n"),
"utf8"
);
chmodSync(launcherPath, 0o755);
if (commandExists("xdg-open")) {
detached("xdg-open", [launcherPath]);
} else {
detached(calculator, []);
}
return launcherPath;
}
let opened;
if (process.platform === "win32") {
opened = windowsShortcutProof();
} else if (process.platform === "darwin") {
opened = macLauncherProof();
} else if (process.platform === "linux") {
opened = linuxLauncherProof();
} else {
throw new Error(`Unsupported platform: ${process.platform}`);
}
console.log("marker: calc-pop-attempted");
console.log(`opened: ${opened}`);

View File

@@ -0,0 +1,42 @@
# Renderer Chain Skeleton
This is a non-executable outline. It intentionally omits a working payload.
## Preconditions To Validate In A Private Lab
- A private Modrinth project or controlled API fixture can return raw HTML in
project `body` or version `changelog`.
- Lunar Explore renders that content in the packaged launcher.
- The injected frame can access the exposed `window.lunar` or
`window.electron` preload bridge from the rendered context.
- The main Redux bridge accepts a forged `profiles/addOrUpdateProfile` action.
- `installModpack` accepts the forged profile ID.
- Override extraction writes root `overrides/*` files to the controlled
effective game directory.
- `openExternalLink` reaches `shell.openExternal` for a local launcher file URL
with a non-restricted initiator.
## Non-Executable Flow
1. Build a virtual profile object with these properties:
- `id`: fresh local ID
- `type`: `modrinth`
- `provider`: `modrinth`
- `state`: `virtual`
- `useLunarFeatures`: compatible with target Modrinth version
- `modrinth.projectId`: controlled test project
- `modrinth.selectedVersion.versionId`: controlled test version
- `overrides.gameDirectory`: writable test directory
2. Send a profile-add action into the main Redux state-sync channel.
3. Invoke the Lunar Modrinth install API for that profile ID.
4. Confirm the controlled test `.mrpack` root override is written under the
chosen game directory.
5. Invoke the Lunar external-link API with a local `file:` URL to the benign
launcher file.
6. Confirm the calculator pop or marker file.
## Expected Benign Result
The validation succeeds only if a benign calculator pop or marker file is
observed. Do not test with an arbitrary command or payload outside a controlled
lab.