XSS
出现位置、测试顺序、编码绕过,以及「弹框」和「真的能用」之间的距离。
成立条件只有一个
用户输入被输出到页面上,且没做正确的转义。
所有的变体都是这条的变形:反射型是输出在响应里,存储型是输出在数据库里,DOM 型是输出在前端脚本里、根本不经过服务端。
先找位置,再想 payload
新手常犯的错是背了一堆 payload,到处试,试不出来就放弃。顺序反了。
正确的顺序是:先输入几个特殊字符("'<>),看它们出现在页面的哪个位置、周围是什么、有没有被转义。
位置决定 payload:
- 出现在 HTML 标签之间 → 直接构造标签
- 出现在属性值里(
value="xxx")→ 先闭合引号,再闭合标签 - 出现在
<script>里 → 不用标签,直接构造 JS 语句 - 出现在
href、src、action里 → 试伪协议javascript:
这几条判断,比记住二十个 payload 有用。
容易被忽略的输入点
能输入的地方都可能有,但有几类特别值得看:
富文本编辑器。公告、邮件正文、工单描述、评论。编辑器允许的标签集合往往很宽,而存储时不做二次过滤。这类功能出问题的比例,比其他输入点高得多。
上传的文件名。上传一个叫 <img src=1 onerror=alert(1)>.png 的文件,如果列表页直接输出文件名而不转义,就成立了。
文件内容本身。允许传 PDF、SVG、HTML 的地方,文件打开就是在浏览器里渲染。SVG 里可以直接写 script。
邮件。内部邮件系统的正文、主题、发件人名。这类系统经常忘了自己也是个 Web 应用。
一个真实的绕过过程
某个后台的反馈功能,输入 payload 直接 404,说明有过滤。
挨个字符试,发现尖括号被拦。换 Unicode 编码 \u003c,不拦,但前端不解析——服务端没做那一步解码。
转去试标题字段。标题在列表页输出时,右键看 HTML,尖括号没转义,但引号位置有闭合问题。先打一个双引号,输出也没处理。
到这里就清楚了:这个字段处在属性上下文里,且不转义引号。最终构造:
\"\u003cinput/onfocus=_=confirm,_(123)\u003e
\u003c 被服务端解码成 <,input 标签带 onfocus 自动聚焦触发,alert 被过滤就用 confirm,括号被干扰就用 _ 当临时变量。四个绕过点叠在一起,才打进去。
这个过程没什么技巧,就是把「哪里被拦」一个个试出来。写出来是为了说明:绕过不是一个 payload,是一串判断。
编码顺序决定能不能绕
浏览器解码的顺序是固定的:
HTML 解码 → URL 解码 → JavaScript 解码
服务端过滤和浏览器解码的顺序不一致,就是绕过的空间。这条是理解所有编码绕过的根。
几个常用的替换:
| 被拦 | 换成 |
|---|---|
alert |
confirm、prompt、console.log |
| 空格 | /**/、换行、%0a |
| 引号 | 反引号、String.fromCharCode |
| 括号 | 反引号模板、throw onerror=... |
| 尖括号 | Unicode、HTML 实体(需服务端先解码) |
alert 的变形可以写得很花:top['al'+'ert'](1)、[].find(alert)、(alert)(1)。目的只有一个——让过滤规则匹配不到,让 JS 引擎认得。
危害取决于上下文
弹框是最低级的证明,报告里写「存在 XSS,可弹框」基本等于没说。
往下走一步:能读到什么?document.cookie 有没有 HttpOnly?能不能发起同源请求?能不能读页面里的敏感数据?
存储型 XSS 如果出现在后台,且管理员会看到,那就是「管理员会话劫持」。如果出现在用户之间互相可见的地方,那就是「蠕虫」的起点。这两种定级完全不同。
修复
四条,按优先级:
- 输出时按上下文转义。HTML 上下文转
<>&"',属性上下文还要处理引号,JS 上下文用JSON.stringify。不要试图在输入时过滤——输入时的过滤永远有遗漏。 - 富文本用白名单。允许哪些标签、哪些属性,写死。黑名单一定会漏。
- Cookie 加 HttpOnly。降低被劫持的后果,不解决漏洞本身。
- CSP。最后一道,配置正确能挡住大部分。但很多站点的 CSP 写得很宽,
unsafe-inline一开就等于没有。
参考
- 《XSS 测试利用总结(附个人常用 payload)》,公众号 起凡安全
- 《XSS 攻击在新花样》,公众号存档
- 《几个有趣的 bypass 挖洞案例》,公众号 渗透安全HackTwo