前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
struts 2.0.11.1 + tomcat7,源码部分不需要改变。
漏洞利用
阅读官方通告,漏洞原因在于 struts2 在读取静态文件时进行了一次 URL 解码,所以可以通过将 ../ 进行两次 URL 编码从而列目录或者访问其他目录中的文件。
测试 payload:
http://localhost:8080/struts/..%252f
可以看到列出了 tomcat 的 lib 目录,读取 class 文件:
http://localhost:8080/struts/..%252f..%252f..%252fWEB-INF/classes/com/example/struts2/LoginAction.class/
漏洞分析
在官方通告中已经明确指出了漏洞存在的类为 FilterDispatcher,我们直接调试即可,找到漏洞方法 doFilter:
if (serveStatic && resourcePath.startsWith("/struts")) {
String name = resourcePath.substring("/struts".length());
findStaticResource(name, request, response);
}
在访问前缀为 /struts 时,会去掉前缀然后调用 findStaticResource:
if (!name.endsWith(".class")) {
for (String pathPrefix : pathPrefixes) {
InputStream is = findInputStream(name, pathPrefix);
...
}
...
}
访问的静态文件不能以 .class 结尾,其实这个限制没有什么用,然后遍历配置好的静态文件目录并调用 findInputStream,tomcat7 的环境下一共有三个目录:
org/apache/struts2/static/
template/
org/apache/struts2/interceptor/debugging/
官方给的 payload 只有其中的 template 能触发漏洞,在 findInputStream 方法中:
protected InputStream findInputStream(String name, String packagePrefix) throws IOException {
String resourcePath;
if (packagePrefix.endsWith("/") && name.startsWith("/")) {
resourcePath = packagePrefix + name.substring(1);
} else {
resourcePath = packagePrefix + name;
}
resourcePath = URLDecoder.decode(resourcePath, encoding);
return ClassLoaderUtil.getResourceAsStream(resourcePath, getClass());
}
将访问的路径跟目录拼接在一起,然后 URL 解码,再调用 getResourceAsStream 开始读取文件,就造成了目录遍历漏洞。
漏洞修复
加上了 URL.getFile 和 endWith 来判断后缀。
其他的思考
为什么 LoginAction.class 后面加上一个 / 还能读取到文件
因为最后会调用 WinNTFileSystem 类的 normalize 方法对路径进行调整:
public String normalize(String path) {
int n = path.length();
char slash = this.slash;
char altSlash = this.altSlash;
char prev = 0;
for (int i = 0; i < n; i++) {
char c = path.charAt(i);
if (c == altSlash)
return normalize(path, n, (prev == slash) ? i - 1 : i);
if ((c == slash) && (prev == slash) && (i > 1))
return normalize(path, n, i - 1);
if ((c == ':') && (i > 1))
return normalize(path, n, 0);
prev = c;
}
if (prev == slash) return normalize(path, n, n - 1);
return path;
}
会消除掉最后面的所有 /。
为什么回退一次目录会访问到 tomcat 的 lib 目录
调试可以发现,findResource 可以用来获取类,而根据资源路径获取资源的顺序是一个从最底层 WebappClassLoader 开始(即 getResource 方法中调用的 Thread.currentThread().getContextClassLoader().getResource(resourceName)),再到最高层,后面逐级下降的顺序(后面就遵循双亲委派)。
而此时的路径为 template/../,首先会交给 WebappClassLoader 来获取资源,而 WebappClassLoader 负责的是项目中的用户类,获取资源的根目录为 WEB-INF/classes,这时候会调用 WebappClassLoader 的 findResource 来处理:
ResourceEntry entry = (ResourceEntry)this.resourceEntries.get(name);
if (entry == null) {
if (this.securityManager != null) {
PrivilegedAction<ResourceEntry> dp = new WebappClassLoader.PrivilegedFindResourceByName(name, name);
entry = (ResourceEntry)AccessController.doPrivileged(dp);
} else {
entry = this.findResourceInternal(name, name);
}
}
在缓存的 resourceEntries 哈希表未命中的情况下,会调用 findResourceInternal 进行下一步处理:
String fullPath = this.repositories[i] + path;
Object lookupResult = this.resources.lookup(fullPath);
这里的 repositories 只有唯一的 /WEB-INF/classes/,在跟资源路径拼接后会调用 lookup 开始获取资源,到了 FileDirContext 类的 file 方法中,经过 canonicalize0 方法的处理后会获得处理后的路径为 D:\Java1.8\struts2\target\struts2\WEB-INF\classes,然后就发生了一个问题:
String fileAbsPath = file.getAbsolutePath();
if (fileAbsPath.endsWith(".")) {
fileAbsPath = fileAbsPath + "/";
}
String absPath = this.normalize(fileAbsPath);
canPath = this.normalize(canPath);
if (this.absoluteBase.length() < absPath.length() && this.absoluteBase.length() < canPath.length()) {
absPath = absPath.substring(this.absoluteBase.length() + 1);
if (absPath == null) {
return null;
}
if (absPath.equals("")) {
absPath = "/";
}
canPath = canPath.substring(this.absoluteBase.length() + 1);
if (canPath.equals("")) {
canPath = "/";
}
if (!canPath.equals(absPath)) {
return null;
}
}
但是这里的 absPath 的最后面是会有一个 / 的,这就导致了获取到的 file 永远是空的,也就是说 WebappClassLoader 这里是获取不到资源的,接下来就会委派给最高层的 ExtClassloader,可以阅读一下这篇文章,我们继续调试,给 ClassLoader 类的 getResource 方法和 URLClassPath 类的 findResource 方法下断点,会发现这是一个循环比较的过程:
public URL findResource(String var1, boolean var2) {
int[] var4 = this.getLookupCache(var1);
URLClassPath.Loader var3;
for(int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
URL var6 = var3.findResource(var1, var2);
if (var6 != null) {
return var6;
}
}
return null;
}
...
URL findResource(String var1, boolean var2) {
Resource var3 = this.getResource(var1, var2);
return var3 != null ? var3.getURL() : null;
}
...
Resource getResource(String var1, boolean var2) {
if (this.metaIndex != null && !this.metaIndex.mayContain(var1)) {
return null;
}
}
...
Resource getResource(String var1, boolean var2) {
if (this.isClassOnlyJar && !var1.endsWith(".class")) {
return false;
}
}
但是因为路径不是 .class 结尾的,所以最后都会失败。
ExtClassloader 无法获取资源,就会交给它的孩子 AppClassLoader, AppClassLoader 同样使用 JarLoader,也同样找不到,这两个 classloader 都是从 Jar 中获取资源的。
接下来交给 URLClassLoader,它可以从文件夹中获取资源,根目录在 tomcat 的 lib 目录,而在它的 getResource 方法中:
Resource getResource(final String var1, boolean var2) {
try {
URL var4 = new URL(this.getBaseURL(), ".");
final URL var3 = new URL(this.getBaseURL(), ParseUtil.encodePath(var1, false));
if (!var3.getFile().startsWith(var4.getFile())) {
return null;
} else {
if (var2) {
URLClassPath.check(var3);
}
final File var5;
if (var1.indexOf("..") != -1) {
var5 = (new File(this.dir, var1.replace('/', File.separatorChar))).getCanonicalFile();
if (!var5.getPath().startsWith(this.dir.getPath())) {
return null;
}
} else {
var5 = new File(this.dir, var1.replace('/', File.separatorChar));
}
return var5.exists() ? new Resource() {...}
}
}
this.dir、this.getBaseURL() 就是 tomcat 的 lib 目录,调用 getCanonicalFile 之后,最后返回的就是 lib 这个目录的资源,所以我们就会读取到 lib 目录下的文件名。
同理,template/ 上向上跳一级,所以我们如果向上跳 4、5 级,同样可以访问到 tomcat 的 lib 目录,只不过这时触发的路径就不是 template 了,而是其他两个配置好的目录:
org/apache/struts2/static/
org/apache/struts2/interceptor/debugging/
继续思考,为什么回退两级会是 404?
首先,确定最下面的 WebappClassLoader 肯定也会因为前面说的路径匹配不上而没有东西,上面两级 JarLoader 的肯定是没有东西的,所以直接去看 URLClassLoader,上面两个 org/apache 的目录肯定是访问不到文件的,所以直接看 template,错误发生在这里:
URL var4 = new URL(this.getBaseURL(), ".");
final URL var3 = new URL(this.getBaseURL(), ParseUtil.encodePath(var1, false));
if (!var3.getFile().startsWith(var4.getFile())) {
return null;
}
因为 template/ 往上跳了两级,所以这里的 var3 会跳到 lib 上一级的 tomcat 目录,跳出根目录导致没有东西。
剩下的跳 >5 次就比较简单了,6、7 次的时候,两个 4、5 级的目录加上 WEB-INF 和 classes 刚好 6、7 次,所以 WebappClassLoader 获取资源时刚好跳到根目录,即 IDEA 的编译目录 /D:/Java1.8/struts2/target/struts2,而用来切割然后比较的根目录 absoluteBase 为 D:\Java1.8\struts2\target\struts2,所以 canPath 会在切割完之后为空,而代码会加上一个 /,所以就跟 absPath 相同了,也就可以目录穿越访问到项目根目录了。
参考文章:
https://blog.csdn.net/how_interesting/article/details/80091472
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!