
研究揭示glibc的getaddrinfo可解析伪装成乱码的IP地址
来源标题:Pushing the Limits with Getaddrinfo
安全研究者演示了glibc的getaddrinfo()函数能接受用上标、下标、圆圈数字和隐藏字符混写的IPv4地址,例如一串看似乱码的字符会被解析成69.30.206.130并成功ping通。该实验在Ubuntu 24.04、glibc 2.39环境下完成,说明任何仅凭字符串外观判断地址的安全检查都可能被绕过。作者指出IPv4地址本质是32位整数,点分十进制只是书写约定,解析规则源自inet_aton()。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
一个 IPv4 地址不过是一个 32 位数字,而 glibc 会接受你根本认不出来的写法。⁰𝑋⁴5.🯰3₆。⁵₂₈𝟲🯶 就是其中一种。任何靠“看起来像不像”来判断地址的东西,都能被绕过去。
这是一次真实的 ping,打向一台真实的机器,并且收到了真实的回复。ping -c1 后面那串东西混合了 LCD 段式数字、上标、下标、带圈数字、一个双线体 X 以及数学字母数字,还夹着九个不可见字符——我的终端把它们画成了白框,因为它实在不知道还能拿它们怎么办。把它粘贴进终端,看起来就像线路噪声。交给 getaddrinfo(),它就是 69.30.206.130。
本文中的每个示例都指向同一台服务器,69.30.206.130。它会回应 ping,并且在裸 IP 上只提供一个页面 /poc.html,这样你也能验证 HTTP 这一侧同样可用。每张截图都是真实输出,采集自 Ubuntu 24.04,使用 glibc 2.39、iputils 20240117 和 curl 8.5.0。
先从最无聊的部分说起,而它已经比大多数人以为的更古怪。一个 IPv4 地址是一个 32 位无符号整数。点分四段只是我们约定俗成的书写方式。把文本转换成这个整数的解析器是 inet_aton(),它的规则可以追溯到 4.2BSD。在 glibc 中,它位于 resolv/inet_addr.c,每一部分都这样读取:
/* Collect number up to ``.''. Values are specified as for C: 0x=hex, 0=octal, isdigit=decimal. */ ... unsigned long ul = __strtoul_internal (cp, &endp, 0, 0);
基数 0 意味着 strtoul() 会根据前缀选择进制:0x 是十六进制,前导 0 是八进制,其他都是十进制。每个点分段都有自己的前缀,所以可以在一个地址里混用不同进制。然后是段数规则:
/* Internet format: a.b.c.d a.b.c (with c treated as 16 bits) a.b (with b treated as 24 bits). */
一段就是整个 32 位数字。两段是 8 位网络加 24 位主机。三段是 8、8 和 16 位。这些本来是为旧的 A/B/C 类网络设计的,从来没人把它们移除。前导零也不受限制,只要值仍然放得下。
以上都是众所周知的东西。这正是 SSRF 过滤器应该解析地址、而不是用正则匹配地址的原因。好玩的部分始于一个大多数人从不留意的标志:AI_IDN。
AI_IDN 是 glibc 的扩展,它告诉 getaddrinfo() 这个名字可能是国际化域名,因此应在查询前先转换为 ASCII。ping 会设置它,因为 iputils 在构建时启用了 IDN 支持。下面是 glibc 在 sysdeps/posix/getaddrinfo.c 中如何处理它:
if (req->ai_flags & AI_IDN) char *out; result = __idna_to_dns_encoding (name, &out); if (result != 0) return -result; name = out; malloc_name = true; if ((result = text_to_binary_address (name, req, &res)) != 0) goto free_and_return;
注意顺序。IDNA 转换先运行,数字地址解析器只能看到它的输出。__idna_to_dns_encoding() 把字符串交给 libidn2,由它应用 UTS #46 映射。UTS #46 的存在,是为了让用户用不同宽度或风格输入的域名仍然落到同一个名字上。为此,它把大量兼容字符折叠成普通 ASCII。那张表并不关心结果随后会被当作数字来读取。
2473 ; mapped ; 0032 0030 # CIRCLED NUMBER TWENTY ⑳ -> 20 325A ; mapped ; 0033 0030 # CIRCLED NUMBER THIRTY ㉚ -> 30 24EA ; mapped ; 0030 # CIRCLED DIGIT ZERO ⓪ -> 0 1FBF6 ; mapped ; 0036 # SEGMENTED DIGIT SIX 🯶 -> 6 1D7CE ; mapped ; 0030 # MATHEMATICAL BOLD DIGIT ZERO 𝟎 -> 0 1D501 ; mapped ; 0078 # MATHEMATICAL BOLD SCRIPT SMALL X 𝔁 -> x 216D ; mapped ; 0063 # ROMAN NUMERAL ONE HUNDRED Ⅽ -> c 1D49 ; mapped ; 0065 # MODIFIER LETTER SMALL E ᵉ -> e 3002 ; mapped ; 002E # IDEOGRAPHIC FULL STOP 。 -> .
数字被覆盖了,点也被覆盖了。十六进制前缀里的 x 以及 a–f 这些十六进制字母同样被覆盖,这意味着上面每一种经典写法都能用 Unicode 形近字符重新拼出来:
带圈两位数是个不错的细节:⑥⑨.㉚.⑳⑥.⑬⓪ 比它解码出的普通地址还短四个字符。十六进制同理,用双线体 x 作前缀,用罗马数字一百代替 c,用修饰字母代替 e:
注意 ping 在它的 PING 行里回显了 0x451ece82。ping 还会请求 AI_CANONIDN,所以它打印的是 IDNA 映射之后的名字。这是你能得到的、关于解析器实际解析了什么的最清晰视图。
没有任何规定说一个地址必须坚持一种风格。映射是逐字符进行的,所以每个数字、点和十六进制字母都可以来自不同的区块。我写了一个小脚本,读取 IdnaMappingTable.txt,收集所有映射到 0–9、a–f、x 或 . 的码点,然后为每个字符随机挑选一个形近字符,重建每一种经典写法:全角、上标、下标、带圈、数学粗体、无衬线、等宽、双线体、手写体、哥特体、罗马数字,以及从 🯰 到 🯹 的 LCD 段式数字。每一个结果都能解析:
这里还有几个,全是 69.30.206.130,可以直接粘贴:
ping -c1 𝟼𝟵.🯳𝟶。𝟚𝟘🯶.𝟏𝟥⓪ # 69.30.206.130 ping -c1 🯰𝖝⁴𝟝。⁰𝚇𝟙𝗲。0ₓ𝖢e。⓪𝑋𝟖🯲 # 0x45.0x1e.0xce.0x82 ping -c1 𝟢𝓧④𝟱₁ᵉⒸ𝘌🯸🯲 # 0x451ece82 ping -c1 01⓪₅。𝟘🯳𝟞.₀3𝟙𝟞。𝟶𝟤₀𝟮 # 0105.036.0316.0202 ping -c1 𝟞𝟗。𝟑𝟢。🯵𝟤₈𝟼6 # 69.30.52866 ping -c1 𝟨⑨.𝟬𝕩𝟏E𝐜ᵉ🯸𝟸 # 69.0x1ece82 ping -c1 ⁰𝑋⁴5.🯰3₆。⁵₂₈𝟲🯶 # 0x45.036.52866 ping -c1 𝟣🯱𝟻⑨6𝟜⑥🯸𝟝𝟎 # 1159646850 ping -c1 𝟬x🯴𝟝.𝟹𝟬.₀3𝟣6。𝟎Ⅹ🯸𝟚 # 0x45.30.0316.0x82
隐形墨水
UTS #46 并不只做字符映射。它还有一个叫 ignored 的类别,被归为 ignored 的码点会直接从输入中删除:
00AD ; ignored # SOFT HYPHEN 200B ; ignored # ZERO WIDTH SPACE 2064 ; ignored # INVISIBLE PLUS E0100..E01EF ; ignored # VARIATION SELECTOR-17..VARIATION SELECTOR-256 1BCA0..1BCA3 ; ignored # SHORTHAND FORMAT LETTER OVERLAP..UP STEP
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。