目录
这是什么?
XSS 系列开篇。WebApp 渗透测试是一个大话题,之前的 SQL 系列之后还会继续深入。本篇的 XSS 系列也一样,在后续的工作学习中,不断记录。
我不再讲 XSS 的基本原理,网上有太多的教程。
最权威的学习资料,列举在下面:
OWASP,Rapid7,Protswigger(Burp Suite)三大家。
如果对于 XSS 的基本概念不太熟悉,建议先阅读这三篇文章,再继续下面的内容。
今日目标
今天,我们一起把 XSS Game 这个有 6 个等级的 XSS 实战小教程做完。理论结合实践,是最快的学习方式。
所有的 6 个等级,都要求我们使用 XSS 让浏览器弹出一个 alert
,即可过关。
在开始之前,先推荐两个个 JSBin HTML JS 线上测试工具 和 JSFiddle。在学习的过程中,如果有代码需要测试,可以很方便地在两个工具上运行。
例如你知道了一个输入框中的用户输入会被直接嵌入到 <strong></strong>
标签中,你想确认在 <strong>
标签中,<script>
是否能被成功执行。那就可以到 JSBin 做个小测试。
另外,HTML URL Online Encoder Decoder 也是非常有用的工具,可以将需要在地址栏注入的脚本做 url encode。
最后再推荐两个 XSS Payload Cheet Sheet,OWASP XSS Cheet Sheet,XSS Cheet Sheet From Github
Level 1
Level 1 的页面是这样的。
XSS Game 模拟了一个浏览器在页面中央。页面底部的 toggle
,点击可以查看页面的源代码。
点击 show
,可以显示提示信息。
Level 1,展示了最基本的反射型 XSS 攻击。这里,有两种方式可以判断开发者有没有对用户输入做校验。
第一种方式,看源码。
可以看到,没有任何的用户输入校验。后端代码从 request
中获取 query
参数,然后直接拼接到 html <b> tag
中间。
另一种方法,输入测试。
对于网页应用来说,最最敏感的字符,就是两个尖括号,<
和 >
。所以我们可以直接输入一个 <test
测试以下是否有过滤机制。
有点奇怪的结果,但是我觉得这是浏览器的行为,跟环境没有关系。我的浏览器也是 Chrome,可以做个小测试。
我写了一个简单的 html 页面。
页面模拟了Level 1 的情况,在 <b>
标签中间,加上了 <test
作为用户输入。
我们启动一个 http server。
我在 Windows WSL 下运行的 Ubuntu,它的 IP 是 192.168.1.108。用浏览其访问该测试页面。
可以看到一样的情况。
换成其他标签也一样。
无论怎么说,可以肯定的是,后端没有做用户输入的校验,导致我们可以直接使用 <script>
注入 javascript。
因此,在搜索框中输入:
<script>alert(1);</script>
或者在 url 中(?query=)输入:
# html url encode
%3Cscript%3Ealert%281%29%3B%3C%2Fscript%3E
即可。
Level 2
Level 2 是典型的持久型 XSS。
同样的步骤,我们看一下源码。
存储评论的时候,没有做任何输入校验,直接将用户输入存储到数据库。
显示评论的时候,同样没有做用户输入的校验处理,直接将用户输入拼接到 DOM。
根据前一 Level 的经验,我们也输入一个 <script>alert(1);</script>
就可以了。但是这里不行。
innerHTML
innerHTML 可以设置 DOM 树中的节点的内容。
var ele = document.getElementById('id');
ele.innerHTML = 'this is not an attack';
innerHTML的安全问题
innerHTML 将字符串解析为 HTML,意思是,如果字符串中有脚本,浏览器就有可能会执行。
为什么 <script>
在 innerHTML 中无法执行,我们看一下文档。
根据 Mozilla 官方的文档,规定 <script>
在 innerHTML 中是无法执行的,仅此而已。
同一篇文档中,Mozilla 也给出了我们需要的答案,我们仍旧可以使用 事件(Event)
来触发 javascript 脚本。
Event
Event,也称 DOM Event,用来通知开发者一些特定的操作或者事件被触发了。比如,onmouseover
事件会在鼠标移入的时候被触发,onload
事件会在页面或者节点被加载的时候被触发,即将使用的 onerror
在有错误发生的时候被触发。
事件被添加到节点上之后,根据事件性质的不同,会在不同的情况下被触发。
如 Mozilla 官方文档所说,在 Level 2 的情况下,我们只需要使用一个事件,就可以触发 alert 弹窗。
在评论区输入:
<img src=# onerror="javascript:alert(1);" />
# 或者
<img src=# onerror="alert(1);" />
# 或者,连分号都不用
<img src=# onerror="alert(1)" />
持久型的 XSS 意味着每当你访问这个页面,弹窗都会执行。可以尝试刷新以下页面,弹窗会再次出现。
Level 3
看源码找漏洞。
location.hash
图片来自 Mozilla Location 官方文档。
这个 hash 大家都见到过,它就像一个面包屑一样,指出你正在阅读的当前页面上的那一部分内容。
比如大家到我的个人博客,阅读 从头开始写操作系统-启动扇区与内存的关系及内存寻址的应用 一文,你随便点一个目录,就会在 URL 后面看到一个 hash。
hash 是 Location 对象的一个属性,代表的,就是上图例子中的 #boosecbyte
部分。
我们结合 Level 3 的源码看一下。
Level 3 中,chooseTab
方法接收 location.hash.substr(1)
作为参数,也就是上 #example
中 example
的部分。
之后,这个 example
,被直接拼接到了 html 中,然后 jQuery 将 html 设置给 tabContent 节点来展示。
没有看到任何用户输入的校验。
我首先尝试了以下直接使用 <script>
进行注入,但是没有效果,说明还是要用事件来做。
有了 Level 2 的经验,我们只需要构造一个一样的 <img> tag
,在错误发生的时候,执行弹窗即可。
在 URL 的 #
之后输入
whatever.jpg' onerror='alert(1)'/>
即可。
Level 4
看源码。
后端在获取 timer
参数的时候,没有做任何字符校验,获取之后,直接传递给 timer.html
渲染。
在 timer.html
中,onload
事件触发后直接将后端获取的 timer
参数的值传递给 startTimer()
方法,作为参数。
onload
事件,会在图片加载完成的时候调用。所以,可以在 onload
事件中,构造一个 XSS 注入。
我们需要做的是构造一个输入,补全 startTimer('
部分,然后注入 XSS 代码,例如 onload="startTimer('3'); alert('1');"
黑体加粗的部分就是我们需要构造的输入。
在输入框输入
3'); alert('1
# 或者
3')+alert('1
或者用上面的 HTML URL Encoder Decoder 工具,编码之后,在地址栏输入
?timer=3%27%29%3B+alert%28%271
# 或者
?timer=3%27%29%2Balert%28%271
即可。
Level 5
我们点击 sign up 跳转到注册页面。
看源码,注册页面选的时候,直接使用后端获取的 next
参数,没有任何字符校验和过滤。
我们输入了 email,点击 Next >>
的时候,实际上是点了一个 <a>
链接,href
属性是上图中获取的 next
参数的值。
因此,直接在浏览器地址栏输入
javascript:alert(1);
点击 Next >>
即可。
Level 6
最后 Level 6,还是对 location.hash
进行利用。
跟 Level 3 类似,在 getGadgetName
方法中,获取 #
之后的部分,如果没有用户输入,默认为 /static/gadget.js
。
接着,获取到的内容传递给 includeGadget()
方法最为参数。
includeGadget()
方法中,首先创建了一个 script
DOM 节点,接着,上一步获取的 #
之后的的部分,作为 script
节点的 src
属性。
最后,这个 script
节点被添加为 head
的子节点。
以默认情况为例,创建的 script
节点为
<script src="/static/gadget.js" />
因此,我们要传递一个恶意的 js 文件的路径,输入到 URL 上 #
的后面,替换掉 /static/gadget.js
即可。
自己没有服务器的同学,可以使用 google 的 callback jsapi。
另外,还有一个点需要注意,如果直接输入地址栏中复制出来的带有 https
或者 http
的 URL,是会报错的。
因为代码中(见前文第 21行 代码)对 http
和 https
进行了过滤,所以,有两种解决方案。
- 不要协议部分
- 将协议部分全部大写或者随意大写一个字符
Protocol-relative URLs
在 html 中使用 URL 可以不需要协议名的部分。例如,<a href="https://www.google.com" />
,可以写成 <a href="//www.google.com" />
。根据 wiki 所述,这样的使用方式叫 Protocol-relative URLs
,对目标地址的访问会使用当前页面的协议。
因此,在地址栏输入
//www.google.com/jsapi?callback=alert
# 或者
httpS://www.google.com/jsapi?callback=alert
即可。
Data URLs
还有另外一种方式,使用 Data URLs
。
Data URLs 是一种协议,可以让开发者在文档中嵌入文件。
Data URLs 由 4 部分组成:
data:
,前缀mediatype
,数据类型base64 token
,非必要,用于嵌入 base64 编码的二进制data
,数据本身
我先在我本机上做了测试,XSS 脚本不只是在 head
节点中可用,添加到任意节点中均可执行。
所以,在地址栏输入
data:text/plain;charset=utf-8,alert%281%29
同样可以弹窗。
总结
- 最基本的 XSS 漏洞,可直接使用
<script>code</script>
<script>
无法执行的时候,可以选择使用事件来触发 XSS 脚本location.hash
等页面地址相关的操作如果使用不当,也会造成 XSS 漏洞Protocol-relative URLs
允许我们省略协议名,使用当前页面的协议访问目标地址Data URLs
允许我们在 DOM 中嵌入并执行 XSS 脚本- 所有的 XSS 漏洞,都是由于没有对用户输入做校验和筛选造成的;所以,XSS 防御的第一步,就是对所有的用户输入进行校验,过滤
- https://owasp.org/www-community/attacks/xss/#:~:text=Overview,to%20a%20different%20end%20user.
- https://www.rapid7.com/fundamentals/cross-site-scripting/
- https://portswigger.net/web-security/cross-site-scripting
- https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
- https://www.tutorialspoint.com/javascript/javascript_events.htm#:~:text=JavaScript's%20interaction%20with%20HTML%20is,%2C%20resizing%20a%20window%2C%20etc.
- https://jsbin.com/?html,output
- https://jsfiddle.net/
- https://www.url-encode-decode.com/
- https://www.w3schools.com/js/js_events.asp
- https://developer.mozilla.org/en-US/docs/Web/Events
- https://www.w3schools.com/tags/ev_onclick.asp
- https://stackoverflow.com/questions/7761149/dynamically-added-script-will-not-execute
- https://developer.mozilla.org/en-US/docs/Web/API/Location
- https://www.w3schools.com/jsref/event_onload.asp
- https://github.com/rorymurphyza/GoogleXSSGame
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
- https://github.com/payloadbox/xss-payload-list
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet