前言:
记录以下UTF-8 Overlong Encoding导致的安全问题;
UTF-8
UTF-8 就是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度
UTF-8 的编码规则:
- 对于单字节的符号,字节的第一位设为
0
,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。 - 对于
n
字节的符号(n > 1
),第一个字节的前n
位都设为1
,第n + 1
位设为0
,后面字节的前两位一律设为10
。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
分析
假如有一个恶意类(如下)
1 | package org.zIxyd; |
如果存在一处代码,可以反序列化这个类,将会导致任意命令执行;
1 | package org.zIxyd; |
但是这处代码有一层waf:!string.contains("Calc")
;
可以看到正常序列化时,序列化的数据会包含className
;
接下来调试,看看反序列化时怎么拿到className
的,1ue师傅已经给出了调用栈:
1 | ObjectStreamClass#readNonProxy(ObjectInputStream in) |
最后是由ObjectInputStream
类下的readUTFSpan
方法;
1 | private long readUTFSpan(StringBuilder sbuf, long utflen) |
其中通过switch (b1 >> 4)
来判断是:多少个字节为一个字符;
我这里的ClassName为:org.zIxyd.Calc
第一个字符为o
;其16进制为 0x6f;
根据代码逻辑,会走到处理一个字节对应一个字符的地方;即返回了 o 的char
1 | case 7: // 1 byte format: 0xxxxxxx |
但难道只有 1 byte format: 0xxxxxxx 时才能获取 o 字符串吗,其实不然,处理俩个字节为一个字符的逻辑和处理三个字节的逻辑都可以返回;
1 | case 12: |
这里以两个字节的为列;用python实现:输出一个字母对应的两个字节值
1 | import string |
其中可以得知o 0xc1 : 0xaf
现在将之前那段恶意的序列化十六进制数据,将6F
改成C1AF
,再次反序列化这段数据;
这里需要注意,因为ClassName多了一个字节,对应的长度也要改变;
1 | package org.zIxyd; |
可以看到o
字符已经被混淆了;所以绕过了黑名单
Tools
漏洞分析完了;但是可以想到手动修改ClassName的字节太过麻烦;师傅们用的办法都是重写writeClassDescriptor
方法,再加上将类名Overlong Encoding的逻辑(具体思路可以看看lzstar师傅)
然后看了一下评论,说重写writeUTF
相比之下简单一点(确实简单不少);所以就有了下面这段代码:
1 | import java.io.IOException; |
对比一下混淆之前和混淆之后的CC5
总结
Overlong Encoding导致的安全问题不止局限于java反序列化中,例如:GlassFish在解码URL时,没有考虑UTF-8 Overlong Encoding攻击,导致将%c0%ae
解析为ASCCII字符的.
(点)。利用%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/
来向上跳转,达到目录穿越、任意文件读取的效果。
最后贴一下p神写了一个简单的Python函数,用于将一个ASCII字符串转换成Overlong Encoding的UTF-8编码:
1 | def convert_int(i: int) -> bytes: |