从一个绕过长度限制的 XSS 中,我们能学到什么?
从一个绕过长度限制的 XSS => IDN => IDNA => punycode => NFKC,长文警告!
一个绕过长度限制 XSS
最近收到了一个白帽子提交的 xss,简单来说就是某个展现的值可被 xss,但是这个业务限制了可输入值的长度是 length <= 4
,所以 xss payload 是非常受限的,就算是引用外部 js,域名也不会这么短(后缀至少是 2 位,加上一个 .
就三位了,再加前缀以及闭合的字符肯定超过了),所以一直没人发现。而这个白帽子利用 Unicode 域名:⑭.₨
(长度为 3,加上闭合 '
)绕过了这一限制。对于 ⑭.₨
,浏览器会将其转为 14.rs
进行访问,从而带入 payload 完成 xss。结合前几期那篇利用不可见字符过人的 webshell 来看,Unicode 再次展现了其威力。
浏览器到底怎么了?
经过搜索,这一特性至少在 2016 年就被人用来挖洞了:
虽然上面的推特里加了一个 in MS Edge
,但经过测试,我发现 Chrome、Firefox、Safari 都会这么处理。浏览器到底怎么了?为什么要这样处理?一切的一切都要从 IDN 说起。
IDN 与 IDNA
许多年前,互联网域名只能由 ascii 字母 a-z
、数字和其他一些字符组成。而我们之所以发明域名 + DNS 解析代替 ip 地址就是为了好记。随着整个世界的国际化,以英文为主的域名已经不够满足其他语种的人的需要,国际化域名(Internationalized Domain Name
,即 IDN
)应运而生。IDN 是指部分或完全使用特殊的文字或字母(包括中文、拉丁字母等等非英文字母)组成的域名。
大家如果现在去申请域名,就可以选择中文的前缀或者后缀:
前缀
后缀
那么问题来了,就目前而言,DNS 服务器的解析都由 ascii 码交换,所以 DNS 服务器上并不支持直接的中文域名解析,那怎么办呢?后来,经过专家们对多份方案的讨论与比较之后,IDNA
(Internationalizing Domain Names in Applications)被采纳为正式标准(提案 rfc3490:https://tools.ietf.org/html/rfc3490 ),说明了应用程序在遇到非 ASCII 字符的时候,应该如何处理。
提到 IDNA,就不得不提 punycode
。
Punycode
punycode 的提案见:https://datatracker.ietf.org/doc/html/rfc3492
目前,所有中文域名的解析都需要转成 punycode,然后通过 DNS 协议解析 punycode,这样 DNS 协议便能够支持非 ASCII 字符了(你可以理解为把非 ASCII 字符的 Unicode 转为特定的 ASCII 码)。
这里给一个维基百科上 IDNA 处理流程的例子:
对 Bücher.example
进行 IDNA 编码(此域名具有两个标签,Bücher
和example
):
- 第二个标签
example
是纯 ASCII,保持不动。 - 第一个标签
Bücher
,先将其转换小写的bücher
(其实还有其他操作,可以搜索 Nameprep 算法详细了解,这里就不多说了),然后转为 punycode,得到bcher-kva
。接下来加上xn--
前缀,得到xn--bcher-kva
- 最后拼接得到
xn--bcher-kva.example
当然你也可以用 python:
注意,转为 punycode 只是 IDNA 流程中的其中一步。 如果你不能理解,可以看下这个例子:
当然,现在的浏览器都完美支持 Unicode 域名,会自动转码,甚至是 curl 都支持 IDN 域名:
当然,这个并非百度官方所有。于是,这就引发了另一个问题,Unicode 七七八八的字符多了去了,要是我申请一个与官方网站视觉相似的域名,岂不是就可以美滋滋地钓鱼?
Let's go fishing!
示例:
https://www.аррӏе.com/
与 https://www.аррle.com/
这就是著名的同形异义词攻击
什么?你一眼就能看出来 l
不一样?那么这个呢?
https://аpple.com
与https://apple.com
:
如果你能看出来两个域名的 a
不一样,请您立即联系您所在当地的超能力者协会。
对抗 IDN 钓鱼,浏览器做了什么?
看了上面的例子,你可能会想,这有什么大不了的呢?既然非 ascii 字符,经过 IDNA 协议的流程之后,会转为 punycode,那我统一在浏览器的地址栏显示 punycode 不就得了?
访问 https://www.аррӏе.com/
显示如下:
思路不错,但是别忘了我们为什么要发明 IDN?就是为了方便其他语种的人使用,提升他们的体验。如果在地址栏显示 punycode,那还不如 ascii 字符来的直观。开倒车,咱们不干。
访问 http://百度.公司
显示如下:
那么该怎么办呢?以 Chrome 为例,他们在这里(https://www.chromium.org/developers/design-documents/idn-in-google-chrome )详细介绍了什么时候会显示 punycode,什么时候显示 Unicode 字符。
Unicode 真的是太多了,所以 Chrome 需要利用很多方式去判断一个域名该用哪种方式显示。这样虽然可能会存在漏掉的情况,但是经过这么多年的发展,Chrome 处理方式已经比较完善了,如果你能发现一个字符绕过了他们的判断,那么这个漏洞的价值保守估计 $2k。
这里顺便吹一波 Chrome,体验真的很好:
(什么叫国际浏览器啊?战术后仰.gif)
相比 Chrome,其他的例如 Safari 处理方式就很粗暴了,不在白名单里的域名直接显示 punycode。
哦我的上帝,看看这个处理方式,什么是用户体验?什么是钓鱼?Safari:“你搁谁这卡 bug 呢?”
IDNA 带来的另一个问题
假如现在有一个终极浏览器,搞定了什么时候显示 punycode 什么时候显示 unicode,彻底解决了钓鱼的问题。也还需要面对另一个问题:Unicode 规范化带来的安全隐患,这就回到了最开始的那个 xss。
前面和大家提到了 IDNA 的 Nameprep 算法,相关提案是 rfc3491,里面有关于 unicode 规范化的说明:https://tools.ietf.org/html/rfc3491#section-4。
而大家初二的时候就知道,Unicode 规范化有四种形式:
NFC
: Normalization Form Canonical CompositionNFD
: Normalization Form Canonical DecompositionNFKC
: Normalization Form Compatibility CompositionNFKD
: Normalization Form Compatibility Decomposition
来看一个例子:
可以看到,⑭.₨
的规范化有 2 种结果,而后面两种是不是很像浏览器处理的逻辑?rfc3491 采用的正是 NFKC
!所以,浏览器采用的 IDNA 协议所使用的 unicode 规范化标准正是 NFKC
。
最后,如果你真的很想深究一下 IDNA 协议到底是怎么规定一个 unicode 域名如何转为 punycode 形式的域名,以及在转为 punycode 之前做了什么事情,我十分建议你去看一下 python 的 encodings 源码,路径是:lib/python3.7/encodings/idna.py
,所有的答案都在这里,就看你愿不愿意探索了。
利用场景
根据上面所述,IDNA 基于的 NFKC 本身就会导致各种问题,加上 IDNA 的一些流程,可能造成的安全问题就更多了,例如:
- 绕过字符过滤
- 绕过应用对数据的限制,例如绕过长度限制、绕过黑名单字符过滤、引入恶意字符导致各种注入(例如远古版本的 sqlserver)等等
- 可能造成非预期的跨域问题
- 越权
- 目录穿越
- ssrf
- 钓鱼
- ...
举几个例子:
绕过字符过滤
Python3.x 支持了 Unicode 变量名,而根据官方文档,解释器在做代码解析的时候,会对变量名进行规范化,使用的算法是 NFKC:
https://docs.python.org/3/reference/lexical_analysis.html#identifiers
这样我们就可以利用特殊字符来绕过关键字符串的过滤,从而执行指定的函数,例如:
1
eval == ᵉval
根据同样的思路,我们可以找到以下字符:
1
ᵃᵇᶜᵈᵉᶠᵍʰᵢʲᵏˡᵐⁿᵒᵖ𝐪ʳˢᵗᵘᵛʷˣʸᶻᴬᴮCᴰᴱFᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾQᴿ𝖲ᵀᵁⱽᵂⅩ𝖸Z
当然,这里只列出了其中一种替换情况,Unicode 里还有很多这种规范化之后变成正常字母的字符。
绕过长度限制
1 |
|
SQL 注入
1 |
|
模板注入
1 |
|
命令注入
1 |
|
目录穿越
1 |
|
ssrf
1 |
|
绕过文件后缀限制
1 |
|
url 跳转
1 |
|
越权
1 |
|
当然,有些 payload 不一定通过浏览器触发(例如最开始的那个 xss),这就要求后端进行了 unicode 规范化,如果后端没有规范化输入,则这些 payload 都是无效的。
总之,在数据流传的节点中,一定要有一个节点进行了 unicode 规范化,这种攻击方式才有可能生效。
至于这些字符是怎么找的,推荐一个网站:https://www.compart.com/en/unicode/ 。可以搜索特定字符的相似字符,例如 T
:
1 |
|
最后再推荐一个工具,可以用来 fuzz 这种特殊字符:
https://github.com/h13t0ry/UnicodeToy
有趣的是,(2022.05.23)我发现了这个 repo,准备在本文中引用它的时候,发现它的 repo 里引了本文,太巧了哈哈。
一些想法
- Unicode,永远滴神!
- RFC 文档其实对各种细节说的很清楚,但是它就和官方文档一样,一般没人愿意好好看...这两天看了好几份 RFC 文档,人都快傻了...
- 深挖技术细节其实很费时间与精力,是场孤独的修行。
来呀快活呀