Host/Split Exploitable Antipatterns in Unicode Normalization

Background

Unicode -> ASCII 要经过两个过程

  1. 格式化(标准化字符)
  2. 微小的编码(将Unicode转换成ASCII)

格式化(Normalization)

将多个类似的Unicode字符统一格式化成另一个Unicode字符
hostname中任意的Unicode字符都会转换成Compatibility Composition(KC)格式。因为Unicode字符的不同组合都会影响到视觉上的一些含义,这一步统一格式化操作是为了保证hostname中的字符能唯一的双向渲染出来。
WX20191003-105155@2x_shrink

微小的编码(Punycoding)

将Unicode编码转换成ASCII码
经过上一步的格式化操作后,就可以用Punycode把Unicode字符变成由ASCII Compatible Encoding Prefix(xn--)、原始顺序的字符、一连串的ASCII字符组成的重构符(用于给机器重构出原来的non-ascii字符部分)
WX20191003-124224@2x_shrink
这种转化其实有点像中文域名在url中的处理方式

此外,ASCII -> Unicode 也只需要将punycoding逆向进行一次即可得到。

三种IDNA标准

IDNA2003 / IDNA2008 / IDNA2008 + UTS46
每一个标准都允许不同格式的Unicode字符和定义了不同的格式化规则

The HostSplit Vulnerability

当Unicode字符能直接格式化成合法的ASCII字符时,就不用经过Punycode转换了,但是这种情况下可能就会出现漏洞点了。

Java

WX20191019-153311@2x_shrink

Python

>>> from urllib.parse import urlsplit, urlunsplit
>>> url = 'http://0akarma.c℀.com/some.txt'
>>> url
'http://0akarma.c℀.com/some.txt'
>>> parts = list(urlsplit(url))
>>> host = parts[1]
>>> host
'0akarma.c℀.com'
>>> newhost = []
>>> for h in host.split('.'):
...     newhost.append(h.encode('idna').decode('utf-8'))
...
>>> parts[1] = '.'.join(newhost)
>>> finalUrl = urlunsplit(parts)
>>> finalUrl
'http://0akarma.ca/c.com/some.txt'
>>>

从example可以看到,特定的unicode字符经过.encode('idna').decode('utf-8')会转换成合法的ASCII字符,而无需经过Punycoding,这样就

另一个变种就是

>>> from urllib.parse import urlparse
>>> r='http://bing.com'+u'\uFF03'+':password@products.office.com'
>>> o = urlparse(r)
>>> o.hostname
'products.office.com'
>>> a = r.encode("IDNA").decode("ASCII")
>>> a
'http://bing.com#:password@products.office.com'
>>> o = urlparse(a)
>>> o.hostname
'bing.com'

这个点其实Zedd师傅已经在SUCTF2019率先出了个题了~

The HostBond Vulnerability

IDNA2008允许Unicode转ASCII时使用\u200D(Zero-Width- Non-Joiner)和\u200c(Zero-Width Non-Joiner),所以如果有一个hostname长的像这样'0a' + \u200D + 'karma.com',那它经过处理之后就会变成xn--0akarma-469d.com这样的punnycode,但是如果我们将这样的punnycode经过A-labels到U-labels的转换时,你就会发现hostname变成了0akarma.com,中间那个\u200D不可见了(其实他还在,只是看不到)。这样就会导致HostBond漏洞的出现。
这样可以造成的后果包括但不限于:
用户无法用肉眼去分别url,容易被钓鱼网站利用
如果服务端存在ASCII->Unicode->ASCII的处理逻辑,经过HostBond可以达到绕过目的

Best Practices

如何发现HostSplit

原作者给了两种方式,一种是在hostname中加入可以跳过Punnycode编码的字符,然后利用dns查询通过判断返回结果来确定是否存在该漏洞,eg:http://canada.c℀.bing.com,如果dns查询结果为canada.ca则代表存在该漏洞,反之则不存在。
第二种我认为更简单一些,举例说明:http://a.comX.b.com,如果访问该url时返回的结果是会请求a.com下的X.b.com文件,就代表存在该漏洞,反之如果访问该url时不会发送请求或发送一个请求到包含Punnycode的域名则代表不存在该漏洞。

如何发现HostBond

HostBond出现的地方大多是在UI里,因为需要将A-labels转换成U-labels,然后渲染出来。

修复建议

  • 统一url是通过ASCII解析还是Unicode,不要两个混在一起用
  • 在解析url之前先进行(黑名单)检测
  • STD3ASCIIRule
  • 过滤

加餐列表

-w629
这个其实可以根据实际情况自己Fuzz

Summary

虽说这个Host-Series系列漏洞在某种意义上来讲比较鸡(少)肋(见),但是这也不失为一种拓宽自己思路的方式,反正就是各种膜大佬的骚姿势。

References

Host/Split Exploitable Antipatterns in Unicode Normalization