Tomcat CVE-2017-12615 补丁bypass

分析

补丁原理

当补丁打完之后,我们传入文件的时候,程序做了这样的处理

1
2
3
4
protected File file(String name, boolean mustExist) {
File file = new File(base, name);
return validate(file, mustExist, absoluteBase);
}

调用了validate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected File validate(File file, boolean mustExist, String absoluteBase) {

if (!mustExist || file.exists() && file.canRead()) { // !mustExist = true,进入 if
...
try {
canPath = file.getCanonicalPath();
// 此处,对路径进行规范化,调用的是 java.io.File 内的方法
// 之前的 Payload 中结尾为空格,那么这个方法就会去掉空格
} catch (IOException e) {

}
...
if ((absoluteBase.length() < absPath.length())
&& (absoluteBase.length() < canPath.length())) {
...
// 判断规范化的路径以及传入的路径是否相等,由于 canPath 没有空格,return null
if (!canPath.equals(absPath))
return null;
}
} else {
return null;
}

也就是,如果我们传入jsp[空格] 这样的文件,就会使用file.getCanonicalPath()来进行格式规整,下面的条件就无法通过,所以无法创建文件。

突破

我们进入file.getCanonicalPath()函数

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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;
}
}

我们主要看核心代码,我进行简单梳理:
首先,两个变量useCanonCachesuseCanonPrefixCache默认是等于ture的,这是CachesPrefixCache两个hashmap是否使用的配置变量,首先我们如果传入jsp[空格],那么cachehashmap里是不存在这个key对应的键值的。我们就进入了if里,那么parentOrNull函数在取得文件的路径之后,我们进入这一句resDir = prefixCache.get(dir);明显,prefixCache这个hashmap里是不存在我们的路径的,所以直接向下跳转调用res = canonicalize0(path);这个函数功能就是规范res,去掉了res中的空格。所以就回到上节说的为什么通不过了
那么突破从何入手呢?
首先我们传入正常的123.txt文件,这里都是十分正常通过,和上面一样,到了这里

1
2
3
4
5
6
7
 if (res == null) {
res = canonicalize0(path);
cache.put(path, res);
......
if (f.exists() && !f.isDirectory()) {
prefixCache.put(dir, resDir);
}

程序将res作为path的键值存入了cache(重要),程序判断了文件是否存在,以及文件是否是路径格式,这里我们不满足文件存在的条件,也就没有任何动作
第二遍我们传入123.txt[空格],这里为什么要加空格呢,因为要过

1
2
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就有值了,就会进入

1
2
3
4
5
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(),文件就可以创建成功了

感谢老爷打赏
显示 Gitment 评论
undefined