之前在测试过程中发现低版本的java环境存在00截断,可以用于实现文件后缀校验绕过。
先考虑一下下面这个案例是否存在任意文件下载:
示例中文件目录path不可被外界控制,但是使用外界可控的字符串taskID进行文件路径拼接。对File类进行实例化时,参数直接使用上述拼接后得到的字符串,且没有进行标准化处理和合法性验证。
可能有人会认为,在动态拼接路径后添加硬编码的文件扩展名,并且在下载文件前,验证文件是否存在,即使taskID中存在“../”等危险字符,考虑到硬编码的文件后缀,代码的执行还是会进入文件不存在的分支,从而避免任意文件下载。
但是真实情况并非如此,该代码段是否会造成任意文件下载,要考虑到应用程序运行时使用的java版本。
下面给出示例:
示例一
第一个示例运行在java1.6中,假设参数bytes为上文的taskID(外界可控参数),bytes后拼接硬编码的“.txt”。从运行结果可以看到,通过getCanonicalPath函数获取归一化后的真实路径,并不含我们拼接的“.txt”。这是由于参数bytes中含有0x00,且1.6版本的java中,File类对文件路径校验不够严格,最终生成的文件路径丢弃了0x00后的字符。
示例二
第二个示例运行在java1.8中,可以看到在该环境下,createNewFile函数抛出了异常,无法实现路径穿越。
看过案例后我们再来看一下1.8和1.6的java.io.File有什么不同。(不同操作系统下,FileSystem不同,windows系统使用WinNTFileSystem,类Unix系统下使用UnixFileSystem,下文的fs统一取WinNTFileSystem)
首先明确:java中的String类型不以‘\u0000’作为字符串结束标志,而C/C++中会以0x00等特定字符作为字符数组/串的结束标志。
先看java1.8的File类。
File类的几个构造函数,主要作用是对path和prefixLength这两个成员变量赋值,跟进normalize函数后,发现该函数主要是对文件路径进行处理,主要处理的是“\”和“/”这两个符号,由于这里不是这次分析的重点,就不详细展开。从构造函数可知,若实例化File类的字符串含有‘\u0000’(即ASCII:0x00),则成员变量path中也含有‘\u0000’,同时包含‘\u0000’之后的字符。
以createNewFile函数为例,该函数在真正执行创建文件前,进行了两次检查。第二次检查if(isInvalid())是这次要关注的重点,进入isInvalid函数。
isInvalid函数检查了是否含有‘\u0000’,若含有该字符则返回true,最终导致createNewFile函数抛出IO异常。
File类中大量函数在真正执行文件操作前都调用isInvalid函数进行校验,因此1.8版本中的java中不存在0x00截断问题。不止是File类,java1.8中的FileInputStream、FileOutputStream中,也间接调用了File类的isInvalid函数,提高了文件操作API的安全性。
Java1.6中的File类并没有检查是否含有0x00。
到此为止,可知1.6,1.8两个版本的java.io.File,java层的主要区别在于对文件操作前是否调用isInvalid()函数检查成员变量path中是否含有‘\u0000’。但是并没有解释在创建/删除文件时,为何成员变量中,‘\u0000’后的字符会被丢弃。这里需要进入native层进行分析,这里同样以createNewFile函数为例。
一系列检查通过后,createNewFile函数执行return fs.createFileExclusively(path),这里传入的path即为File类的成员变量path。该方法用于创建文件,逻辑如下:
如果已经存在该文件,尽量不抛出异常,而是返回 false,此过程还会尝试读取该文件的属性,失败则抛IO异常。
在将路径转换成宽字符形式时,由于C/C++中宽字节字符数组/串(wchar_t即WCHAR)会以0x0000为字符串结尾(如下示例)
导致java层传递进来的String:path如果含有‘\u0000’,在
WCHAR *pathbuf = pathToNTPath(env, path, JNI_FALSE);
执行之后,pathbuf就不含有‘\u0000’之后的字符了。
继续执行createFileW时使用了pathbuf参数,创建的文件的真实路径自然不包含‘\u0000’之后的字符。由此出现了00截断现象。
其他
以java为开发语言的web应用程序中,大量使用MultipartFile实现文件上传,以下是个测试demo。
上传文件后抓包修改filename字段,插入byte类型的数据00,发送后服务器报错。
直接跟进fileupload.util.Streams.checkFileName,可见该函数对文件名进行了校验,若文件名中存在‘\u0000’则抛出异常,减少使用MultipartFile.getOriginalFilename()拼接文件名带来的风险。
除此之外CommonsMultipartFile实现getOriginalFilename()函数时,返回值filename取最后一个文件分隔符后的字符串,也能缓解上传文件时跨路径上传的风险。