Tomcat CVE-2017-12615 补丁bypass
分析
补丁原理
当补丁打完之后,我们传入文件的时候,程序做了这样的处理
protected File file(String name, boolean mustExist) {
File file = new File(base, name);
return validate(file, mustExist, absoluteBase);
}
调用了validate
函数
protected File validate(File file, boolean mustExist, String absoluteBase) {
if (!mustExist || file.exists() && file.canRead()) { // !mustExist = true,进入 if
...
try {
canPath = file.getCanonicalPath();
} catch (IOException e) {
}
...
if ((absoluteBase.length() < absPath.length())
&& (absoluteBase.length() < canPath.length())) {
...
// 判断处理后的路径和传入的路径参数是否相等,由于 canPath 把出入的路径参数的空格去掉了,导致canPath和之前的路径不同,return null
if (!canPath.equals(absPath))
return null;
}
} else {
return null;
}
也就是,如果我们传入jsp[空格]
这样的文件,就会使用file.getCanonicalPath()
来进行格式规整,下面的条件就无法通过,所以无法创建文件。
突破
我们进入file.getCanonicalPath()
函数
public String getCanonicalPath() throws IOException {
if (isInvalid()) {
throw new IOException("Invalid file path");
}
return fs.canonicalize(fs.resolve(this));
}
这里调用了fs的canonicalize
函数,fs是对WinNTFileSystem
对象的实例,我们进WinNTFileSystem
看看,发现是Win32FileSystem
的子类。我们主要看canonicalize
函数,进入Win32FileSystem
public String canonicalize(String path) throws IOException {
// If path is a drive letter only then skip canonicalization
//用处不大
int len = path.length();
if ((len == 2) &&
(isLetter(path.charAt(0))) &&
(path.charAt(1) == ':')) {
char c = path.charAt(0);
if ((c >= 'A') && (c <= 'Z'))
return path;
return "" + ((char) (c-32)) + ':';
} else if ((len == 3) &&
(isLetter(path.charAt(0))) &&
(path.charAt(1) == ':') &&
(path.charAt(2) == '\\')) {
char c = path.charAt(0);
if ((c >= 'A') && (c <= 'Z'))
return path;
return "" + ((char) (c-32)) + ':' + '\\';
}
//用处不大
if (!useCanonCaches) {
return canonicalize0(path);
} else {
String res = cache.get(path);
if (res == null) {
String dir = null;
String resDir = null;
if (useCanonPrefixCache) {
dir = parentOrNull(path);
if (dir != null) {
resDir = prefixCache.get(dir);
if (resDir != null) {
// Hit only in prefix cache; full path is canonical,
// but we need to get the canonical name of the file
// in this directory to get the appropriate capitalization
String filename = path.substring(1 + dir.length());
res = canonicalizeWithPrefix(resDir, filename);
cache.put(dir + File.separatorChar + filename, res);
}
}
}
if (res == null) {
res = canonicalize0(path);
cache.put(path, res);
if (useCanonPrefixCache && dir != null) {
resDir = parentOrNull(res);
if (resDir != null) {
File f = new File(res);
if (f.exists() && !f.isDirectory()) {
prefixCache.put(dir, resDir);
}
}
}
}
}
return res;
}
}
我们主要看核心代码,我进行简单梳理:
首先,两个变量useCanonCaches
和useCanonPrefixCache
默认是等于ture
的,这是Caches
和PrefixCache
两个hashmap
是否使用的配置变量,首先我们如果传入jsp[空格]
,那么cache
的hashmap
里是不存在这个key对应的键值的。我们就进入了if
里,那么parentOrNull
函数在取得文件的路径之后,我们进入这一句resDir = prefixCache.get(dir);
明显,prefixCache
这个hashmap
里是不存在我们的路径的,所以直接向下跳转调用res = canonicalize0(path);
这个函数功能就是规范res
,去掉了res
中的空格。所以就回到上节说的为什么通不过了.
那么突破从何入手呢?首先我们传入正常的123.txt文件,这里都是十分正常通过,和上面一样,到了这里
if (res == null) {
res = canonicalize0(path);
cache.put(path, res);
......
if (f.exists() && !f.isDirectory()) {
prefixCache.put(dir, resDir);
}
程序将res
作为path
的键值存入了cache
(重要),程序判断了文件是否存在,以及文件是否是路径格式,这里我们不满足文件存在的条件,也就没有任何动作。
第二遍我们传入123.txt[空格]
,这里为什么要加空格呢,因为要过这个条件:
String res = cache.get(path);
if (res == null) {
如果path相同,cache
中已经存在了这个path
对应的键值,就不会进入if了。我们使用123.txt[空格]
便过了这一点,之后,就正常进入程序运行,但是在后面,因为canonicalize0
处理掉了res
的空格,所以当实例化file
时,file.exists()
的返回值变为ture
了,那么进入了prefixCache.put(dir, resDir);
,那么prefixCache里便存在了dir。
第三次再次传入123.jsp[空格]
时,正常走程序,当到了resDir = prefixCache.get(dir);
的时候,这里由于第二次我们在prefixCache
里传入了值,resDir
就有值了,就会进入:
if (resDir != null) {
String filename = path.substring(1 + dir.length());
res = canonicalizeWithPrefix(resDir, filename);
cache.put(dir + File.separatorChar + filename, res);
}
这里是不会对文件整理的,就这样,我们绕过了之前的absoluteBase.length() < canPath.length()
,文件就可以创建成功了