深入剖析XSS和HTML、URL、JavaScript编码(一)

写这篇文章的由来是读了《深入理解浏览器解析机制和XSS向量编码》这篇文章,觉得写得很好,我看了很多遍,但是感觉还是有些混乱,所以就想自己调研并写一篇更加明白的将XSSHTML、URL、JavaScript编码说清楚的文章。一方面留给自己,另一方面抛砖引玉,引出潜在的大神出来纠正错误,讨论更好的理解方法。因为该话题涉及的篇幅较长,打算拆分成若干篇幅来讨论,主要分为HTML解析、URL解析、JavaScript解析、通过编码防御XSS等几个部分。

XSS的危害不必多说,其中防御XSS的一个很重要的措施便是输出编码,那么如何编码才能保证不会出现XSS,其实和上下文(context)有着紧密的关系。我理解的上下文就是环境的概念,处在什么环境会发生解码,什么环境脚本才会执行,之前我一直不是很清楚,后面查了大量资料,在这个专题中就来仔细谈一谈。

HTML解析

XSS在HTML中的执行环境

为了搞清楚XSS和HTML解析的关系,我查看了部分HTML5标准文档,并画了一些图辅助理解。首先我们谈一下有哪些机制可以使攻击者提供的可执行代码在文档的上下文中运行,HTML5的Scripting部分提到,包括但不限于以下:

  1. <script>标签
  2. javascript: URLs
  3. 事件处理Event handlers
  4. 自我脚本技术SVG

所以要想执行XSS脚本,必须满足以上一种执行环境,这篇文章我们只要谈第一种,<script>标签的情况。

HTML的元素

HTML元素(element)在语法上由标签(tag)表示的,通过标签才能识别出元素的种类。其实标签和后面要谈的状态联系紧密,所以先看看HTML中通过标签是如何将元素分类的。下图是HTML5中的分类,主要为6种元素,空元素、原始文本元素,RCDATA元素、外部元素、基本元素和模板元素,如下图:

图片描述

上图中的第三种元素叫做RCDATA,它的定义如下:

The term RCDATA elements refers to elements within which character references are supported, but all other content is treated as raw text instead of markup.

意思是支持字符引用(character reference,下面我们谈字符引用),其他类型的内容被视为文本的元素,RCDATA包括<title>和<textarea>内的元素,根据字面意思理解,支持字符引用应该就是可以转义编码的意思。

第二种元素叫做原始文本元素(Raw text element),也就是纯文本的元素,这意味着其他内容(如注释,字符引用和其他元素)无法在HTML语法中表示,也就是不能转义编码后的文本。原始文本元素包括<script>和<style>内的元素,看下面的代码

<script>
function isLeap(year) {
    return year % 4 == 0 &amp;&amp; year % 100 != 0 || year % 400 == 0;
}
console.log(isLeap(2019))
</script>

这个是执行不了的,$amp;在<script>标签中不能被转义为&,<script>标签内的是纯文本元素。现在我们还不清楚,转义了是否就可以执行,其实转义和执行是两码事,后面我们会仔细剖析,我们先看看字符引用的定义。

字符引用(Character Reference)

字符引用主要是通过转义(escape)来表示文本无法正常表达的字符,例如分数 ½ 的转义是&#x00BD;字符引用必须以U+0026(&)开头;在防范XSS中是为了转义一些危险的字符,字符引用有以下三种表达类型:

  1. 命名字符引用(Named character references),以&开头,加上name,参考这里,例如&lt;,以U+003B(;)结束,这个也叫作HTML实体(HTML Entity);
  2. 10进制数字字符引用(Decimal numeric character reference),格式为&#后面跟一个或更多0-9的字符,以U+003B(;)结束;
  3. 16进制数字字符引用(Hexadecimal numeric character reference),格式为&#x(或者&#X)后面跟一个或更多0-9A-Fa-f的字符,以U+003B(;)结束;

字符引用和HTML元素

上面那个图其实谈到一些字符引用和HTML元素的关系,这里我们再详细探讨一下字符引用在哪些元素中可以被转义。上面我们已经谈过了原始文本是无法转义字符引用的,RCDATA是可以转义字符引用的,如下:

<textarea>&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;</textarea>

可以在浏览器中的调试元素中看到渲染之后,RCDATA已经变为了<script>alert(1)</script>,但是转义之后变为原始文本元素,无法执行。外部元素和模板元素我们以后再谈,然后谈基本元素。基本元素定义如下,来自HTML5参考draft版

Normal elements can contain text, character references, other elements, and comments, but the text must not contain the less-than sign character (<) or an ambiguous ampersand.

从定义可以看出基本元素中不能包含<和歧义的&,可以包含文本、字符引用、注释等其他元素,字面上理解这里的字符引用是可以被转义的,如下:

<h1>&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;</h1>

这个也是可以转义出来的,上面讨论了这么多,发现RCDATA基本元素都是可以转义字符引用的,那么满足什么条件才会切换到执行脚本的环境呢,这个需要了解HTML的解析。

HTML解析与XSS执行

XSS的执行环境其实和一个叫做状态机的概念息息相关的,状态机必须以data state开始,我们看看HTML是如何分词的,引用自HTML5标准:

Most states consume a single character, which may have various side-effects, and either switches the state machine to a new state to reconsume the current input character, or switches it to a new state to consume the next character, or stays in the same state to consume the next character. Some states have more complicated behavior and can consume several characters before switching to another state.

这段话的意思是状态机消耗一个字符会产生多种可能的影响:

  1. 切换到一个新的状态,重新消耗现在的字符;
  2. 切换到一个新的状态,消耗下一个字符;
  3. 维持状态不变,消耗下一个字符;
  4. 切换到一个新的状态,消耗多个字符;

我们主要用到的是其中的1、2,绝大多数是第2种情况,情况其实这种状态的改变和字符的消耗就是在解析输入的字符流,输出构建Document tree,正好就是下图中的tokenizer的过程:
parsing-model-overview.png
可以看到输入字符流不仅来自网络的返回,还来自脚本的执行,也就是说脚本是可以改变DOM树的。HTML解析比较抽象,下面这个图可能看起来更简单一点,我把它贴出来了,来自这里
browser_parse_html.png
其实我的理解是,HTML解析器一边解析比特流(Bytes->Characters->tokens),一边开始构建DOM tree,把node压栈,<html>是第一个被压栈的node,直到解析完毕才会弹出。在解析的过程中会出现释放token的情况,某个token被释放之后,必须立即被tree构建状态处理,因为tree构建会影响解析的状态,举个例子:如果token是<script>被释放(emit)的话,tree构建状态会立马将环境切换到一种叫做script data state的状态,这个就是脚本执行状态,后面会仔细说。

状态(state)

之前谈到转义和执行关系不大,而执行和解析状态有着直接关系,我们先看看状态。其实HTML状态机中的状态数目非常多,我这里只列举一些,包括Data state、RCDATA state(RCDATA状态)、RAWTEXT state、Script data state(脚本数据状态)、PLAINTEXT state、Tag open state、End tag open state、Tag name state等,有大概80个,详细状态查看这里。关于转义,就涉及字符引用,那么看看字符引用是怎么被转义的,而且搞清楚字符引用和执行到底有没有关系。关于状态,墙裂建议看一看HTML5标准中的tokenization这一节,否则理解起来有些困难。

字符引用状态

根据HTML5标准,我们分析一下转义中的状态转移,假设的例子是

<...>
&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;
<...>

例子是<script>alert(1)</script>转义后的结果,放置在html中,从data state开始输入stream处理,如下图

clipboard.png

初始状态是data state(数据状态),第一个字符是&,消耗&进入character reference state(字符引用状态)并且设置好返回状态为data state用作后面使用;然后根据上面的代码是#,消耗#,进入numeric character reference state(数字字符引用状态),同理,消耗x,进入hexadecimal character reference start state(16进制字符引用开始状态),消耗3C,进入hexadecimal character reference state(16进制字符引用状态),这里不是很重要,略去了复杂度,其实是将字符3C转为十进制的数字后放入一个叫做character reference code中去;然后消耗;,进入numeric character reference end state(数字字符引用结束状态),后面检查,character reference code放入buffer,释放(emit)buffer中的字符token,并返回最先设置的返回状态,即data state。重复后面的字符引用&#x73;&#x63;...&#x3E;直到结束,都没有将script作为一个整体token释放,只是将字符引用一个一个解析后一个token一个token释放。这就是为什么转义了之后,没有执行环境的原因。

简单总结一下,当<script>没有做编码时,会将script整体作为一个token释放,然后就会进入script data state状态;但是如果编码后,就会将script看成文本,拆成一个一个字符释放(转义后为s作为token释放,又进入字符引用状态,继续转义后为c作为token释放,......,最后是t),所以到最后也没进入script data state状态,也就是没有执行环境。

我们终于知道了<script>标签转义后是进入不了执行环境的,那么如何进入<script>执行环境呢?我们可以分析一下,如果需要script data state,就需要<script>标签,进而需要Tag open state状态,HTML5唯一进入Tag open state状态的是Data state消耗<字符,所以条件就是Data state加没有转义的<script>标签。逻辑如下图:
图片描述
当然除了script data state,通过释放标签还会进入其他状态,如下:

  1. 进入RCDATA state,通过释放<title>、<textarea>标签;
  2. 进入RAWTEXT state,通过释放<style>、<xml>、<iframe>、<noembed>、<noframes>标签;
  3. 进入script data state,通过释放<script>标签;
  4. 如果scripting flag启用,<noscript>会切换到RAWTEXT state状态 ,否则会进入data state
  5. 进入PLAINTEXT state,通过释放<plaintext>标签;
  6. 其他标签的释放进入data state

这个和上面的HTML元素有些类似,可能HTML元素的分类是借鉴了这里吧。

进入<script>执行环境

上面说了HTML5唯一进入<script>执行环境的是Data state加<script>标签,所以要考虑所有进入Dara state状态的方式。我这里大致总结了一下,进入Data state的方式有以下情况:

  1. Tag open state,参考HTML5文档,除了! / ?和字母,anything else都被视为parse error,释放<,进入data state,所以下面这个是可以执行的,感兴趣的可以试一下<12561,.';\][22<script>alert(1)</script>;
  2. End tag open state,消耗>会被视为parse error,进入data state,所以</><script>alert(1)</script>也是可以执行的;
  3. Tag name state,消耗>,释放标签token(当然这里的标签是前面刚讨论的进入data state,不是<script>这类的特殊标签),进入data state;

还有很多,我画了一个图,描绘了主要的进入data state的状态:
图片描述
所以遵循HTML状态的转移过程,无论多么复杂和奇怪的内容也可以判断出后面的<script>脚本块是否可以执行。理解了这个我们再返回来看看哪些标签可以转义字符引用,也就是进入字符引用状态(character reference state):

  1. data state:消耗&,设置return state为data state,然后切换到character reference state;
  2. RCDATA state:消耗&,设置return state为RCDATA state,然后切换到character reference state;
  3. Attribute value (double-quoted) state:消耗&,设置return state为attribute value (double-quoted) state,然后切换到character reference state;
  4. Attribute value (single-quoted) state:消耗&,设置return state为attribute value (single-quoted) state,然后切换到character reference state;
  5. Attribute value (unquoted) state:消耗&,设置return state为attribute value (unquoted) state,然后切换到character reference state;

所以能转义字符引用的就三种data stateRCDATA state还有attribute value state(单引号、双引号或无引号)。我们下面测试一下,data state就不说了,方式太多了:

<!--1. RCDATA state的<textarea>标签,编码部分是<script>alert(1)</script>-->
<textarea>&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;</textarea>
<!--2. 属性状态(无引号),编码部分为><script>alert(1)</script>-->
<div id=1 name=&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;>1</div>
<!--3. 属性状态(双引号),编码部分为"><script>alert(1)</script>-->
<div id=2 name="&#34;&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;">2</div>

测试结果是上面3个都可以转义字符引用,但是都是一个一个字符转义后释放,没有将script整体作为一个token释放,这里就不多说了。其实今天我们只谈论了一种<script>脚本执行环境的情况,但是背后却有这么多的backgroud做支撑,所以XSS的姿势不是随便蒙的,理解了原理才是真的了解为什么XSS在这里执行了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章