在学习xss漏洞的过程中我发现一个问题,当我想绕过过滤机制时,可以采用编码的方式进行绕过这种方法,但是并不是每一种编码格式都能绕过,需要不停的尝试才行,这样过于浪费时间。后来我发现浏览器与服务器数据传输过程中有好几种编码格式,不同的编码格式有着不同的解析引擎,作为一个浏览器,在解析一篇HTML文档时主要有三个处理过程:HTML解析,URL解析和JavaScript解析。每个解析器负责解码和解析HTML文档中它所对应的部分,其执行的先后顺序往往决定了输出的结果。
浏览器接收到页面数据,于是开始进行HTML 解析,构造DOM树。构造的过程与语言的编译过程是相似的,接收文档,先进行词法分析,然后语法分析,构建解析树。
一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个’<‘符号(后面没有跟’/'符
号)就会进入“标签开始状态(Tag open state)”。然后转变到“标签名状态(Tag name state)”,“前属性名状态(before attribute name state)”…最后进入“数据状态
(Data state)”并释放当前标签的token。当解析器处于“数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
这里有三种情况可以容纳字符实体,“数据状态中的字符引用”,“RCDATA状态中的字符引用”和“属性值状态中的字符引用”。在这些状态中HTML字符实体将会从“…”形式解码,对应的解码字符会被放入数据缓冲区中。例如,在问题4中,“<”和“>”字符被编码为“<”和“>”。当解析器解析完“
字符实体是一个转义序列,它定义了一般无法在文本内容中输入的单个字符或符号。一个字符实体以一个&符号开始,后面跟着一个预定义的实体的名称,或是一个#符号以及字符的十进制数字。
字符实体类似于如下所示:
&entity_name;
或
entity_number;
如需显示小于(>)号,我们必须这样写<
或<
或<
注意:
使用实体名而不是编号的好处是,名称易于记忆。不过坏处是,浏览器也许并不支持所有实体名称(对实体编号的支持却很好),某些字符没有实体名称,但可以有实体编号。
实体名称对大小写敏感!
在HTML中,某些字符是预留的,预留字符必须被替换为字符实体,一些在键盘上找不到的字符也可以使用字符实体来替换。例如在HTML中不能使用“<”或“>”,这是因为浏览器可能误认为它们是标签的开始或结束。如果希望正确地显示预留字符,就需要在HTML中使用对应的字符实体。
在这三种情况中 HTML字符实体将会从“…”形式解码,对应的解码字符会被放入数据缓冲区中
字符引用包括“字符值引用”和“字符实体引用”。在上述HTML例子中,‘<’ 对应的字符值引用为<
,对应的字符实体引用为<
字符实体引用也被叫做“实体引用”或“实体”。)
要了解什么是RCDATA,我们先要了解另一个概念。在HTML中有五类元素:
空元素(Void elements),如,
等等
,
原始文本元素(Raw text elements),有和
RCDATA元素(RCDATA elements),有和
外部元素(Foreign elements),例如MathML命名空间或者SVG命名空间的元素
基本元素(Normal elements),即除了以上4种元素以外的元素
五类元素的区别如下:
空元素,不能容纳任何内容(因为它们没有闭合标签,没有内容能够放在开始标签和闭合标签中间)。
原始文本元素,可以容纳文本。
RCDATA元素,可以容纳文本和字符引用。
外部元素,可以容纳文本、字符引用、CDATA段、其他元素和注释
基本元素,可以容纳文本、字符引用、其他元素和注释
如果我们回头看HTML解析器的规则,其中有一种可以容纳字符引用的情况是“RCDATA状态中的字符引用”。这意味着在和
标签中的字符引用会被HTML解析器解码。这里要再提醒一次,在解析这些字符引用的过程中不会进入“标签开始状态”。这样就可以解释如下问题了
<textarea><script>alert(5)</script>textarea>
被HTML解析器解码后:
HTML解析器将<
和>
进行了解码,解码为 ‘<’ 和 ‘>’
注意:
对RCDATA有个特殊的情况。在浏览器解析RCDATA元素的过程中,解析器会进入“RCDATA状态”。在这个状态中,如果遇到“<”字符,它会转换到“RCDATA小于号状态”。如果“<”字符后没有紧跟着“/”和对应的标签名,解析器会转换回“RCDATA状态”。这意味着在RCDATA元素标签的内容中(例如或
的内容中),唯一能够被解析器认做是标签的就是或者
。因此,在
和
的内容中不会创建标签,就不会有脚本能够执行。这也就解释了为什么下列语句中的脚本不会被执行。
<textarea><script>alert(6)script>textarea>
浏览器访问服务器,首先需要的便是URL地址来指示服务器资源的位置,浏览器向服务器发送请求信息,获取响应结果。
URL解析引擎能解析URL编码,而HTML与js 的解析引擎无法解析URL编码,因此就算是把
执行失败
\u0027 转义为单引号,但是仅仅是被解释成单引号文本,而此时字符串是未闭合的。
问题14
执行成功
\u000a
在JavaScript里是换行,就是\n
,直接执行
在Java里\u000a
是换行,相当于在源码里直接按一下回车键,后面的代码都换行了
当浏览器从网络堆栈中获得一段内容后,触发HTML解析器来对这篇文档进行词法解析。在这一步中字符引用被解码。在词法解析完成后,DOM树就被创建好了,JavaScript解析器会介入来对内联脚本进行解析。在这一步中Unicode转义序列和Hex转义序列被解码。同时,如果浏览器遇到需要URL的上下文,URL解析器也会介入来解码URL内容。在这一步中URL解码操作被完成。由于URL位置不同,URL解析器可能会在JavaScript解析器之前或之后进行解析。考虑如下两种情况
Example A:
Example B:
在例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:
例C与例A很像,但不同的是在UserInput前多了window.open()操作。因此,对UserInput多了一次额外的URL解码操作。总的来说,四轮解码操作被完成,顺序是HTML,URL,JavaScript和URL。
此时此刻,读者应该已经获得解答博文开始提到的所有问题的必要知识。如果你有任何的问题,欢迎留言讨论。
简而言之,作为攻击者为了弄明白如何让XSS向量逃逸出上下文,或者为了使你的应用能够正确编码用户的输入,你必须真正明白浏览器的解析原理以及它们(HTML,URL和JavaScript解析器)是如何协同工作的。只有这样,你才能从浏览器的角度去正确编码你的向量。