本文最后更新于:星期三, 八月 19日 2020, 2:17 下午

从一个绕过长度限制的 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: Hello World!

许多年前,互联网域名只能由 ascii 字母 a-z、数字和其他一些字符组成。而我们之所以发明域名 + DNS 解析代替 ip 地址就是为了好记。随着整个世界的国际化,以因为为主的域名已经不够满足其他语种的人的需要,国际化域名(Internationalized Domain Name,即 IDN)应运而生。IDN 是指部分或完全使用特殊的文字或字母(包括中文、拉丁字母等等非英文字母)组成的域名。

大家如果现在去申请域名,就可以选择中文的前缀或者后缀:

前缀

后缀

那么问题来了,就目前而言,DNS 服务器的解析都由 ascii 码交换,所以 DNS 服务器上并不支持直接的中文域名解析,那怎么办呢?punycode 便横空出世。

Punycode 与 IDNA

目前,所有中文域名的解析都需要转成 punycode,然后由 DNS 解析 punycode。这样 DNS 便能够支持非 ASCII 字符(你可以理解为把非 ASCII 字符的 Unicode 转为特定的 ASCII 码)。

虽然这样 DNS 解析已经可以支持非 ASCII 字符了,但如果仅仅这样,浏览器、电子邮件等应用程序依旧是无法使用 IDN 的,因为它们所使用的网络协议是不支持非 ASCII 的域名的。于是专家们又提出了 IDNA(提案 rfc3490:https://tools.ietf.org/html/rfc3490),说明了应用程序在遇到非 ASCII 字符的时候,应该如何处理(可以理解为,经过一些特定的步骤后,再转为 punycode)。这里给一个维基百科的例子:
Bücher.example 进行 IDNA 编码(此域名具有两个标签,Bücherexample):

  1. 第二个标签 example 是纯 ASCII,保持不动。
  2. 第一个标签 Bücher,先将其转换小写的 bücher(其实还有其他操作,可以搜索 Nameprep 算法详细了解,这里就不多说了),然后转为 punycode,得到 bcher-kva。接下来加上xn--前缀,得到xn--bcher-kva
  3. 最后拼接得到 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.comhttp://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 规范化有四种形式:

  1. NFC: Normalization Form Canonical Composition
  2. NFD: Normalization Form Canonical Decomposition
  3. NFKC: Normalization Form Compatibility Composition
  4. NFKD: 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 的一些流程,可能造成的安全问题就更多了,例如:

  1. 绕过应用对数据的限制,例如绕过长度限制、绕过黑名单字符过滤、引入恶意字符导致各种注入(例如远古版本的 sqlserver)等等
  2. 可能造成非预期的跨域问题
  3. 越权
  4. 目录穿越
  5. ssrf
  6. 钓鱼

举几个例子:

绕过长度限制
```
In [47]: print(unicodedata.normalize(‘NFKC’, ‘⑭.₨’))
14.Rs

In [48]: len(‘⑭.₨’)
Out[48]: 3


> SQL 注入

In [20]: print(unicodedata.normalize(‘NFKC’, ‘' or '1'='1’))
‘ or ‘1’=’1
In [21]: print(unicodedata.normalize(‘NFKC’, ‘" or "1"="1’))
“ or “1”=”1
In [22]: print(unicodedata.normalize(‘NFKC’, ‘admin\’ ﹣﹣’))
admin’ —


> 模板注入

In [39]: print(unicodedata.normalize(‘NFKC’, ‘﹛﹛3+3﹜﹜’))
6
In [40]: print(unicodedata.normalize(‘NFKC’, ‘[[5+5]]’))
[[5+5]]

> 命令注入

In [41]: print(unicodedata.normalize(‘NFKC’, ‘&& whoami’))
&& whoami
In [42]: print(unicodedata.normalize(‘NFKC’, ‘|| whoami’))
|| whoami


> 目录穿越

In [28]: print(unicodedata.normalize(‘NFKC’, ‘‥/‥/‥/‥/etc/passwd’))
../../../../etc/passwd
In [29]: print(unicodedata.normalize(‘NFKC’, ‘︰/︰/︰/︰/etc/passwd’))
../../../../etc/passwd

> ssrf

In [33]: print(unicodedata.normalize(‘NFKC’, ‘①②⑦.⓪.⓪.①’))
127.0.0.1


> 绕过文件后缀限制

In [34]: print(unicodedata.normalize(‘NFKC’, ‘test.pʰp’))
test.php


> url 跳转

In [38]: ‘tr0y。wang’.encode(‘idna’)
Out[38]: b’tr0y.wang’

In [39]: print(unicodedata.normalize(‘NFKC’, ‘https://evil.c℀.baidu.com‘))
https://evil.ca/c.baidu.com

In [40]: ‘https://evil.c℀.baidu.com'.encode('idna‘)
Out[40]: b’https://evil.ca/c.baidu.com


> 越权

In [46]: print(unicodedata.normalize(‘NFKC’, ‘uname=ªdmin’))
uname=admin


当然,有些 payload 不一定通过浏览器触发(例如最开始的那个 xss),这就要求后端进行了 unicode 规范化,如果后端没有规范化输入,则这些 payload 都是无效的。

**总之,在数据流传的节点中,一定要有一个节点进行了 unicode 规范化,这种攻击方式才有可能生效。**

至于这些字符是怎么找的,推荐一个网站:https://www.compart.com/en/unicode/。可以搜索特定字符的相似字符,例如 `T`:

![](https://rzx1szyykpugqc-1252075454.piccd.myqcloud.com/IDN/20200818065913116.png!blog)

In [51]: print(unicodedata.normalize(‘NFKC’, ‘ᵀ’))
T
```

一些想法

  1. Unicode,永远滴神!
  2. RFC 文档其实对各种细节说的很清楚,但是它就和官方文档一样,一般没人愿意好好看…这两天看了好几份 RFC 文档,人都快傻了…
  3. 深挖技术细节其实很费时间与精力,是场孤独的修行。

来呀快活呀


经验总结      Web

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

DNS 安全(一):基础知识复习 上一篇
从算数扩展到 RCE 下一篇