0x00 前言
自我发布 《探索高版本 JDK 下 JNDI 漏洞的利用方法》这篇文章后就开始找基于org.apache.catalina.users.MemoryUserDatabaseFactory
“创建目录”环节的利用链,因为那篇文章我用的是 h2 数据库的类(对这个背景不了解的可以先看一下那篇文章),不是特别完美。所以我打算再找一个更通用的。我从4ra1n发给我的含有public无参构造扫描结果中挨个看了一遍,然后从中找到了一个非常有趣的链,但实践测试时发现JDK的deploy.jar
不会被 Tomcat 加到 classpath。这也就让这条链降低了实战价值,不过我挖掘它的过程非常具有分享的价值,能够在一定程度上对不了解这类问题的人有所启发,所以写了这篇文章供参考。
0x01 发现过程
当我在看 com.sun.deploy.cache.Cache
这个类的时候顺手搜了一下 mkdir
关键字,发现它的 reset() 方法有去创建目录,cachePath,是从 com.sun.deploy.config.Config#getCacheDirectory
获取的路径和 /6.0
拼接组成文件名。
如果我想修改 cachePath 那就要看看com.sun.deploy.config.Config#getCacheDirectory
是怎么获取的,还要想办法修改它。
跟进该方法,发现它首先调用了一个 get()
方法,然后再调用get()
返回对象的getProperty(String)
方法去查询deployment.user.cachedir
。
跟进get()
它去调用了com.sun.deploy.config.DefaultConfig#getDefaultConfig
(顺便提一下com.sun.deploy.config.DefaultConfig
和com.sun.deploy.config.Config
是继承关系),这里有一个Java基础的知识点,它用的是一个线程安全的Lazy(俗称懒汉式)单例模式获取对象,也就是说通过 com.sun.deploy.config.DefaultConfig#getDefaultConfig
获取的对象,他们实际上都是同一个对象,如果我修改了这个对象的某个值,那么其他地方调用的这个单例对象获取相应值时也会发生改变。
虽然这个地方看起来没有什么问题,但写过 Java 单例模式的人应该知道,通常单例模式的构造方法要改成 private 修饰的,而DefaultConfig
类的构造方法不但是无参构造,还是 public 修饰的,也就意味着我可以不通过 getDefaultConfig()
来实例化DefaultConfig
对象,这里是一个重要的知识点,后面会串联起来。
前面讲了deployment.user.cachedir是怎么获取的,因为我需要创建指定的目录,那我就必须修改它。
刚好 com.sun.deploy.config.Config
类就提供了一个setCacheDirectory(String)
方法修改它。同样是先调用了get()
方法来获取一个单例的com.sun.deploy.config.DefaultConfig
对象,然后调用它的setProperty(String,String)
方法来修改deployment.user.cachedir
。
所以应跟进com.sun.deploy.config.DefaultConfig#setProperty(String,String)
这里并没有直接去修改 Key Value,而是加了一个条件判断 _inInit
必须是true
再来看看 _inInit
是如何赋值的。
重点我都标记在了图片上,也就是说当 DefaultConfig
对象被实例化的那一瞬间,会让_inInit=true
当实例化结束_inInit
就变成了 false
让我们把前面的重点都串联起来,整理一下结论。
首先我的目的是调用DefaultConfig.setCacheDirectory("/tmp/test/blueDirectory/"),以修改deployment.user.cachedir
的值为/tmp/test/blueDirectory/
∵因为_inInit
在正常情况下等于false无法成功修改deployment.user.cachedir
∵又因为_inInit
是类静态变量没有做线程安全保护
∵又因为实例化DefaultConfig
的一瞬间 _inInit=true
∴所以分别开启多个线程在实例化DefaultConfig
对象的同时调用DefaultConfig.setCacheDirectory("/tmp/test/blueDirectory/")
就能够让修改deployment.user.cachedir
时通过_inInit==true
的条件,达成”条件竞争“攻击。
综上所述,已经能够解决修改deployment.user.cachedir
问题了。那么在前面com.sun.deploy.cache.Cache#reset
方法调用com.sun.deploy.config.Config#getCacheDirectory
的返回结果也就会发生改变。
现在只需要最后一个点来触发com.sun.deploy.cache.Cache#reset
方法。
让我们再次回到com.sun.deploy.cache.Cache
类,去寻找reset()
方法的触发点。
通过关键词搜索,可以看到reset()
方法是在类加载的过程中在 static 代码块里被调用的。
所以要想触发创建目录的操作,就需要 Class.forName 加载com.sun.deploy.cache.Cache
类,这个类基本不会被用到,所以一般是没有被加载过的。
0x02 场景模拟
public class MakeDir {
static ExecutorService pool = Executors.newCachedThreadPool();
static String orginCacheDirectory = Config.getCacheDirectory();
public static void main(String[] args) throws Exception {
File file = new File("/tmp/test/");
File crack = new File(file, "/crack/");
System.out.println("file count="+file.list().length+" filename="+String.join(",",file.list()));
System.out.println(crack+" exists="+crack.exists());
System.out.println("orgin CacheDir="+orginCacheDirectory);
Runnable runnable = new Runnable(){
public void run() {
DefaultConfig defaultConfig = new DefaultConfig();
}
};
Runnable runnable2 = new Runnable(){
public void run() {
DefaultConfig.setCacheDirectory("/tmp/test/crack");
}
};
pool.submit(runnable);
pool.submit(runnable2);
while (true){
String newCacheDirectory = Config.getCacheDirectory();
if (!newCacheDirectory.equals(orginCacheDirectory)){
System.out.println("changed CacheDir: "+newCacheDirectory);
break;
}
}
Class<?> cacheClass = Class.forName("com.sun.deploy.cache.Cache");
System.out.println(cacheClass);
System.out.println(crack + " exists="+crack.exists());
}
}
我通过用这段代码来模拟从修改 _inInit
到修改CacheDirectory,再到最后的触发目录创建。
因为他们都是按照 JNDI 利用链条件找的,所以 JNDI Reference 对象可以这么写。
最后把代码改成 JNDI 请求再模拟一次同样具有效果。
当目录创建这条链打通了,就可以去打跨目录写文件的链了
0x03 总结
我在 IDE 里能够成功的原因是 classpath 会把 deploy.jar 带上,而 Tomcat 默认不会,所以这条链就没用了,也许只有当某些特殊系统的场景下可能会把 deploy.jar 加到 classpath 里这个链才能体现价值。
另外不管是在挖gadget还是在找漏洞时都可以多关注一些线程安全或是条件竞争类的问题,这类问题通常都不会被注意到。