前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
struts 2.0.11.1 + tomcat7,源码部分不需要改变。
漏洞利用
阅读官方通告,漏洞原因在于 struts2 在读取静态文件时进行了一次 URL 解码,所以可以通过将 ../ 进行两次 URL 编码从而列目录或者访问其他目录中的文件。
测试 payload:
1
| http://localhost:8080/struts/..%252f
|
可以看到列出了 tomcat 的 lib 目录,读取 class 文件:
1
| http://localhost:8080/struts/..%252f..%252f..%252fWEB-INF/classes/com/example/struts2/LoginAction.class/
|
漏洞分析
在官方通告中已经明确指出了漏洞存在的类为 FilterDispatcher,我们直接调试即可,找到漏洞方法 doFilter:
1 2 3 4
| if (serveStatic && resourcePath.startsWith("/struts")) { String name = resourcePath.substring("/struts".length()); findStaticResource(name, request, response); }
|
在访问前缀为 /struts 时,会去掉前缀然后调用 findStaticResource:
1 2 3 4 5 6 7
| if (!name.endsWith(".class")) { for (String pathPrefix : pathPrefixes) { InputStream is = findInputStream(name, pathPrefix); ... } ... }
|
访问的静态文件不能以 .class 结尾,其实这个限制没有什么用,然后遍历配置好的静态文件目录并调用 findInputStream,tomcat7 的环境下一共有三个目录:
1 2 3
| org/apache/struts2/static/ template/ org/apache/struts2/interceptor/debugging/
|
官方给的 payload 只有其中的 template 能触发漏洞,在 findInputStream 方法中:
1 2 3 4 5 6 7 8 9 10 11 12
| 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 方法对路径进行调整:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 来处理:
1 2 3 4 5 6 7 8 9
| 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 进行下一步处理:
1 2
| 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,然后就发生了一个问题:
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
| 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 方法下断点,会发现这是一个循环比较的过程:
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
| 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 方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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 了,而是其他两个配置好的目录:
1 2
| org/apache/struts2/static/ org/apache/struts2/interceptor/debugging/
|
继续思考,为什么回退两级会是 404?
首先,确定最下面的 WebappClassLoader 肯定也会因为前面说的路径匹配不上而没有东西,上面两级 JarLoader 的肯定是没有东西的,所以直接去看 URLClassLoader,上面两个 org/apache 的目录肯定是访问不到文件的,所以直接看 template,错误发生在这里:
1 2 3 4 5
| 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
https://xz.aliyun.com/t/7967