JavaSec系列 - 3. 反序列化与JNDI注入(1)
本章源码: https://github.com/hey3e/JavaSec-Code/tree/main/javasec3
上章我们了解到,通过控制lookup
方法的参数,可以实现JNDI注入。从控制到注入,就是一条完整的攻击链。我们来看一个实例。
首先,我们需要一个注入点。在java.lang
中,我们找到了类com.sun.rowset.JdbcRowSetImpl
,其connect
方法对dataSource
属性作lookup
:
回溯,发现该类的setter
方法setAutoCommit
调用了connect
:
类的getter
和setter
分别用于外界对类的private
属性进行访问和更新。而从外界调用类内方法的手段,我们首先想到了反射,和处处用到反射的反序列化。
因此攻击链的挖掘思路就很清晰了:寻找反序列化操作,使其反射构造恶意的JdbcRowSetImpl
对象并调用setAutoCommit
,从而实现JNDI注入。
我们了解到,JSON解析库fastjson在将JSON字符串还原为Java对象的过程中会调用其getter
和setter
方法。我们来看一下。
构造一个Person
类,包含getAge
和setAge
方法:
1 | package javasec3; |
编写fastjson代码:
1 | public class test { |
其中使用JSON.toJSONString
和JSON.parseObject
进行序列化与反序列化。结果如下:
并无特别之处,同时我们debug发现JSON.parseObject
并没有调用Person
类的getter
和setter
方法。这是因为我们没有实现“自省”功能:SerializerFeature.WriteClassName
,修改下行代码:
1 | String json = JSON.toJSONString(person, SerializerFeature.WriteClassName); |
再次查看结果:
可以看到序列化后的字符串中新增了@type
字段标明了类型信息。再次跟进,发现如期调用了setAge
和getAge
:
也就是说,对于指明@type
的输入,fastjson会调用getter
和setter
方法。
至此,我们得知要构造的恶意JSON payload中需要包含:
1 | { |
再次回到JdbcRowSetImpl
类的setAutoCommit
方法,其调用的connect
方法会对dataSource
属性作lookup
,因此,我们在payload中新增一个dataSource
字段,值为我们启动的RMI/LDAP服务器。同时,既然要进入setAutoCommit
,payload中就必须有autoCommit
:
1 | { |
payload准备就绪。接下来用SpringBoot启动一个web服务,在index页面对传入的JSON字符串作反序列化:
1 |
|
编写恶意类,编译后挂载:
1 | import java.io.Serializable; |
用marshalsec启动一个RMI服务器,返回指向Evil
类的恶意Reference
:
最后,POST payload,计算器弹出:
在lookup
处下断点,调用栈如下:
可见在反序列化过程中,利用反射invoke
了类的setter
方法。
需要注意的是,上述攻击需要fastjson<=1.2.24。在后续的版本中,fastjson加入了checkAutoType
方法和黑名单对输入进行过滤,但也不断被绕过。大部分绕过的原理,都是基于本文所述的反序列化+JNDI注入。
JavaSec系列 - 3. 反序列化与JNDI注入(1)
https://hey3e.github.io/2022/01/17/JavaSec系列-3-反序列化与JNDI注入-1/