以jvm-sandbox为基础 浅谈RASP实现要点(中)

208

本文大都为笔者学习rasp的见解,如有错误,请大佬及时指正。

GitHub - luelueking/jvm-sandbox-rasp: 一个基于jvm-sandbox高度定制化rasp

Contextual Analysis

  • 记得在上一篇提到对Http进行Hook时,当时增强的方法为javax.servlet.http.HttpServlet#service方法,但是在实际测试中可以发现总是会连续的打印2次日志。

    • img
  • 这是为什么?我先给出修改代码,粉色部分则是添加的代码

    • @Override
      protected void before(Advice advice) throws Throwable {
          // 只关心顶层调用
          if (!advice.isProcessTop()) {
              return;
          }
          System.out.println("hook到HttpServlet的service方法");
          // jvm-sandbox 是在独立的 ClassLoader 中运行的,因此需要做一层代理
          HttpServletRequest request = InterfaceProxyUtils.puppet(HttpServletRequest.class, advice.getParameterArray()[0]);
          HttpServletResponse response = InterfaceProxyUtils.puppet(HttpServletResponse.class, advice.getParameterArray()[1]);
          RequestContextHolder.set(new RequestContextHolder.Context(request, response));
          super.before(advice);
      }
      
  • 观察结果,发现只打印了1次,并且Context也成功获取

  • img

  • isProcessTop表示***是否整个递进调用过程中的顶层通知。***出现了多次就代表hook了子类的实现如DispatcherServlet等。而实际上我们只需要关心顶层调用即可。

Thread Inject

  • 先看看线程注入是怎么个事。
    • // thread.jsp
      <%@ page language="java" contentType="text/html; charset=UTF-8"
               pageEncoding="UTF-8"%>
      <%@ page import="java.io.IOException" %>
      
      <%
          // 创建线程执行命令,而不是直接执行命令
          Thread t = new Thread(new Runnable() {
              @Override
              public void run() {
                  try {
                      Runtime.getRuntime().exec("open -a Calculator");
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          });
          t.start();
          out.println(">==test==<");
      %>
      
  • 没错开辟了一个线程绕过了ThreadLocal,导致获取不到Context,从而执行。
  • 这里最简单的解决办法就是使用InheritableThreadLocal,这个类的特点就是在父进程创建子进程的同时,向子进程传递变量。
    • // InheritableThreadLocal 防止线程注入
      private static final InheritableThreadLocal<Context> REQUEST_THREAD_LOCAL = new InheritableThreadLocal<Context>();
      
  • 当然还有其他方法,例如使用TransmittableThreadLocal(alibaba开源的线程池,文档也很完善)。
  • TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。相比InheritableThreadLocal,添加了protectedtransmitteeValue()方法,用于定制 任务提交给线程池时ThreadLocal值传递到 任务执行时 的传递方式,缺省是简单的赋值传递。

Hook JNI

  • JNI注入jsp例子参考https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp
  • 可以看到关键点点是使用了ClassLoader#loadLibrary0来加载jni库,那么可以hook方法。当然在我的jv m-sandbox-rasp中也hook了其他方法。
    • if (loadLib == null || !((Boolean) loadLib)) {
          Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
          loadLibrary0Method.setAccessible(true);
          loadLibrary0Method.invoke(loader, commandClass, jniFile);
          application.setAttribute("__LOAD_LIB__", true);
      }
      
    • img

  • 值得注意的是如果已经被JNI注入过了,后续再attach是无法起到防御效果的,因为jni库已经被加载了,同时恶意native方法也不好感知。

Hook WebSocket

  • 为什么要hook websocket呢。是因为笔者最近挖洞发现ws协议也是非常有意思的利用面。而websocket协议与http不同,如果通过以往的httphook是获取不到Context信息的,也就出现了绕过的风险。那么就来尝试下hook websocket协议吧。
  • 纵观多种Java框架与ws适配,以笔者目前的调研,其底层都是用了javax.websocket下的类,而对于攻击者而言,往往可以控制的是ws中的message。那么这里hookMessageHandleronMessage方法就好。
package javax.websocket;

public interface MessageHandler {
    public interface Whole<T> extends MessageHandler {
        void onMessage(T var1);
    }

    public interface Partial<T> extends MessageHandler {
        void onMessage(T var1, boolean var2);
    }
}
  • 其实我觉得rasp应该也多关注协议方面的问题,如果仅仅是靠http来获取上下文的话还是不够的。类似需要做的还有dubbo通信协议等等。这里只是以webscoket为引子。

Stack Trace

  • 其实RASP做到这个地步,差不多也能摸摸IAST了。IAST我个人目前的理解就是扫描器+RASP,这就要求RASP能输出详细的攻击信息了。虽然前面已经获取到了请求的Context,但还差什么?差点堆栈信息图。那么再来完善一下吧。
  • 这里的做法又有点取巧(参考了JRASP)。先看代码
package com.lue.rasp.utils;

/**
 * 调用栈
 */
public class StackTrace {

    /**
     * RASP自身的栈开始位置
     */
    private final static String RASP_STACK_END = "java.com.alibaba.jvm.sandbox.spy";


    public static String[] getStackTraceString() {
        return getStackTraceString(100,true);
    }
    /**
     * @param maxStack 输出的最大栈深度
     * @return
     */
    public static String[] getStackTraceString(int maxStack, boolean hasLineNumber) {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        // 找到用户栈开始index
        int i;
        for (i = 0; i < stackTraceElements.length; i++) {
            String className = stackTraceElements[i].getClassName();
            if (className != null && className.startsWith(RASP_STACK_END)) {
                break;
            }
        }
        int endIndex = Math.min(i + maxStack, stackTraceElements.length - 1);
        String[] effectiveArray = new String[endIndex - i];
        // 获取有用的栈
        for (int k = i + 1; k <= endIndex; k++) {
            String info = "";
            StackTraceElement tmp = stackTraceElements[k];
            if (hasLineNumber) {
                // 不包含行号
                info = tmp.toString();
            } else {
                info = tmp.getClassName() + "." + tmp.getMethodName();
            }
            effectiveArray[k - i - 1] = info;
        }
        return effectiveArray;
    }

    /**
     * @param maxStack 输出的最大栈深度
     * @return
     */
    public static StackTraceElement[] getStackTraceObject(int maxStack) {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        // 找到用户栈开始index
        int i;
        for (i = 0; i < stackTraceElements.length; i++) {
            String className = stackTraceElements[i].getClassName();
            if (className != null && className.startsWith(RASP_STACK_END)) {
                break;
            }
        }
        int endIndex = Math.min(i + maxStack, stackTraceElements.length - 1);
        StackTraceElement[] effectiveArray = new StackTraceElement[endIndex - i];
        // 获取有用的栈
        for (int k = i + 1; k <= endIndex; k++) {
            effectiveArray[k - i - 1] = stackTraceElements[k];
        }
        return effectiveArray;
    }



}

如果看过(上)篇,应该知道jvm-sandbox通过在BootstrapClassLoader中埋藏的**Spy**类完成目标类和沙箱内核的通讯

img

那么只要获取当前线程的StackTrace信息,然后在用户栈找到Spy埋点的类的位置,包装返回Stack Trace信息

利用StackTrace.getStackTraceString()跟踪简单rce的trace

img

参考