UTF-8编码中的Overlong Encodings问题

编码发展历程

“编码”这个词指将信息、数据等按照一定规则转换成特定的符号或代码,以便于传输、存储和处理。

早期古埃及的象形文字是一种编码形式。发展到了计算机时代,计算机相关数据的处理规则采用二进制编码形式,用0和1两个数字来代表。后来ASCII编码被引入,由于是美国人定制的,所以只表示了字母、数字和符号。
ASCII编码

ASCII编码只有127个字符,27能代表128个字符。所以ASCII只用了七位。1字节(byte)=8位(bits),也就是ASCII编码只需要1字节。但是ASCII无法满足各类语言的需求,就比如中文,汉字超过几万个,7位肯定无法表示这么多的中文字符。后来我国制定了GB2312编码,采用两字节来编码中文。两字节=16位,216能表示65536个字符。类似的,日本采用Shift_JIS编码,韩国采用EUC-KR编码。但是这样也带来了一个问题,就是各国编码之间不兼容,放一起会出现乱码。后来出现的Unicode编码为解决这个问题,定义了一个统一的字符集,把所有语言都整合到一套编码里。Unicode编码一般采用两字节。

但是,这样也有新的问题。ASCII编码的字母A是十进制的65,二进制为01000001,由于Unicode编码是两字节,那么需要在前面用0补位,即00000000 01000001。如果数据都是英文字符,用Unicode编码比ASCII编码多用一倍的存储空间。所以出现了“可变长编码”的设计—UTF-8编码。

UTF-8编码

UTF-8编码可以参考维基百科:https://en.wikipedia.org/wiki/UTF-8#Overlong_encodings

UTF-8用一到四个字节表示字符。常用的英文编码是1字节。这样兼容了ASCII(Unicode前128个字符和ASCII一致),UTF-8也是如此。汉字通常是3字节。

UTF-8编码对照表

wiki这个UTF-8对照表是将Unicode编码转为UTF-8编码的计算方式。
U+0000U+007F意为0-127个字符,对应1字节,以0开头,后面补入7个字符。以此类推2字节、3字节、4字节。

wiki官方给了个欧元符号从Unicode值U+20AC转成UTF-8的计算例子。

CharacterBinary code pointBinary UTF-8Hex UTF-8
€(U+20AC)0010 0000 1010 110011100010 10000010 10101100E2 82 AC

U+20AC位于U+0800U+FFFF之间,因此需要三个字节进行编码(对应上图Byte 3)。然后将十六进制转为二进制。

2(十六进制) = 0010(四位二进制)
0(十六进制) = 0000(四位二进制)
A(十六进制) = 1010(四位二进制)
C(十六进制) = 1100(四位二进制)

上图中Byte 3的格式是三个字节分别以11101010开头,然后分别补入4、6、6位。U+20AC这四个四位的二进制数,按顺序补入即0010(4位)、000010(6位)、101100(6位),然后结合三字节的固定头部,就成为了1110 001010 00001010 101100。然后再分别将这三个二进制转为16进制得到E2 82 AC

Overlong Encodings

编码发展历程中讲到,Unicode是固定的两字节,在使用英文字符时,会浪费一倍的存储空间。所以有了可变长编码设计的UTF-8。这种Overlong Encodings就是可变长度的一种形式,为了兼容各类编码系统设计的。同一个字符可以用不同数量的字节来表示。而所谓的Overlong就是用了比最少字节数更多的字节来编码。例如欧元符号€本身是三个字节,但我们用了四个字节编码。这就是超长编码。

假如例子中的欧元符号 € 要从3个字节编码成4个字节,可以先填充0。4字节编码需要填入的字节数分别是3、6、6、6,一共21位。那么需要在U+20AC的二进制0010 0000 1010 1100(16位)前面加入5个0,变成00000 0010 0000 1010 1100,然后在按照4字节格式填入11110 000、10 000010、10 000010、10 101100,转换成十六进制就是F0 82 82 AC。这就是Overlong Encodings

但是Overlong Encodings是存在一定的安全隐患的,所以修订后的 UTF-8 标准(如 RFC 3629)规定,每个 Unicode 字符必须使用最短的字节序列来进行编码。例如python 3.7中对于无效的 UTF-8 字节流的每个字节都视为错误
python3处理错误的UTF-8

路径中的Overlong Encodings安全问题

CVE-2000-0884就是Overlong Encodings的问题,参考链接:https://capec.mitre.org/data/definitions/80.html

IIS 4.0 and 5.0 allows remote attackers to read documents outside of the web root, and possibly execute arbitrary commands, via malformed URLs that contain UNICODE encoded characters, aka the “Web Server Folder Traversal” vulnerability.

路径中的%C0%AF会被解码为/ ,导致目录穿越。

/scripts/..%c0%af../winnt/system32/cmd.exe

/本身的Unicode是U+002F。url编码就是%2f。但是%2f一般不会被允许在路径中执行,防止被目录穿越。这里就通过Overlong Encodings进行绕过。

U+002F本身是1字节的,二进制位00101111,那么如果扩展成2字节,根据上面的图,需要11位,即补3个0,000 00101111。然后根据两字节的格式,分成110 0000010 1011111。UTF-8就是C0 AF,即%C0%AF,绕过了%2F的限制

服务器解析时,将UTF-8编码形式转换成Unicode编码形式,即将上述转换过程进行倒推。按照2字节的11010开头的格式,分别截取后面的二进制,得到00000101111。然后拼接在一起的到00000101111,此二进制对应的十进制是47,47对应的Unicode是/,实现了跨目录。

同样的Glassfish 4.1之前的版本也受此攻击,参考:https://www.trustwave.com/hubfs/Web/Library/Advisories_txt/A_14137_twsl2015-016.txt?fid=6904

Glassfish Overlong Encodings攻击

Java序列化绕WAF

Java生成序列化数据后,在网络流量侧其实是能看到关键类名的,如TiedMapEntry等。
java序列化数据

所以一般在防御时会对反序列化payload中常用的关键字进行提取,写成如下规则,来检测是否是攻击行为。

pcre:"/org\/apache\/commons\/collections\/Transformer|org\.apache\.commons\.collections(4)?\.functors|java\.lang\.reflect\.Proxy|java\/lang\/reflect\/InvocationHandler/i"

然后@1ue师傅提出了一个思路,常见的检测对类名进行检测,那能否让序列化后的类名不可见,从而绕过关键字检测。

ObjectStreamClass#readNonProxy(ObjectInputStream in)
 -> ObjectInputStream#readUTF()
  -> BlockDataInputStream#readUTF()
   -> ObjectInputStream#readUTFBody(long utflen)
    -> ObjectInputStream#readUTFSpan(StringBuilder sbuf, long utflen)

解析类名的核心方法定位到readUTFSpan,它负责从缓冲区中读取一个 UTF-8 编码的片段,并将其解码为一个 Java 字符串。
readUTFSpan

可以看到这个方法中就根据上面图中UTF-8编码对应的字节格式进行解析的。整体的代码逻辑是,先读取第一个字节 b1,根据b1的前四位决定字符的格式(1字节、2字节或3字节)。如果是单字节,直接存储b1。如果是双字节,检查b2是否是10开头,如果是的话将b1和b2组合起来进行存储。如果是3字节,检查b2和b3是否前两位都是10,是的话将b1,b2,b3组合起来存储。

0xxxxxxx(0-127):单字节格式。
110xxxxx 10xxxxxx(128-2047):双字节格式。
1110xxxx 10xxxxxx 10xxxxxx(2048-65535):三字节格式。

那么思路同样是用Overlong Encodings来绕过。比如上面的类中一般都存在字符oo的十六进制是0x6f(二进制1101111),本身是单字节。如果用2字节进行绕过,先用0扩展成0000 1101111,然后根据格式,变成110 0000110 101111,对应的十六进制为0xC10xAF。这样将0x6F修改为0xC10xAF就可以绕过包含字符o的关键字检测(字符不可见,如下图)。

因为默认的UTF-8解析是采用最小字节进行解析的。所以字节序列中无法解析出对应的字符,但由于反序列化的readUTFSpan方法是按UTF-8实际的编码过程进行解析的,可以正确解析Overlong Encodings,并不影响实际的反序列化。

修改字节序列

但是这种方式需要注意,在修改字节后,需要将字节最前面的长度也进行修改。例如0x6F修改为0xC10xAF多了一字节,那么就需要将长度0x10修改为0x11。Ps:org.example.Evil类名长度16个字节,对应十六进制就是0x10

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值