本文由360云安全-jweny原創發布
原文鏈接:https://www.anquanke.com/post/id/219177

隨著HW、攻防對抗的強度越來越高,各大廠商對于webshell的檢測技術愈發成熟,對于攻擊方來說,傳統的文件落地webshell的生存空間越來越小,無文件webshell已經逐步成為新的研究趨勢。

三月底針對tomcat內存馬的檢測寫了一個demo,但由于對Maven打包理解不深,整個項目結構比較糟糕。

國慶前偶然發現LandGrey師傅的copagent項目,在該項目基礎上進行了重構,并于本文中記錄了檢測思路,以及部分代碼demo。

一、Java內存馬簡介

關于JAVA內存馬的發展歷史,這里引用下 c0ny1師傅的總結 。早在17年n1nty師傅的《Tomcat源碼調試筆記-看不見的shell》中已初見端倪,但一直不溫不火。后經過rebeyong師傅使用agent技術加持后,拓展了內存馬的使用場景,然終停留在奇技淫巧上。在各類hw洗禮之后,文件shell明顯氣數已盡。內存馬以救命稻草的身份重回大眾視野。特別是今年在shiro的回顯研究之后,引發了無數安全研究員對內存webshell的研究,其中涌現出了LandGrey師傅構造的Spring controller內存馬

從攻擊對象來說,可以將Java內存馬分為以下幾類:

  1. servlet-api
  2. filter型
  3. servlet型
  4. listener型
  5. 指定框架,如spring
  6. 字節碼增強型
  7. 任意JSP文件隱藏

為方便學習,webshell demo已整理至github

二、整體思路

無論是以上哪種攻擊方式,從防守方的角度來說,檢測的方式都是通過java instrumentation機制,將檢測jar包attach到tomcat jvm,檢查加載到jvm中的類是否異常。

整體檢測思路為:

  1. 獲取tomcat jvm中所有加載的類
  2. 遍歷每個類,判斷是否為風險類。我們把可能被攻擊方新增/修改內存中的類,標記為風險類(比如實現了filter/servlet的類)
  3. 遍歷風險類,檢查是否為webshell:
  4. 檢查高風險類的class文件是否存在;
  5. 反編譯風險類字節碼,檢查java文件中包含惡意代碼

三、獲取jvm中所有加載的類

  1. 遍歷java jvm,查找所有的tomcat jvm

  2. 通過java instrumentation,將agent attach到每個tomcat jvm。由于可能存在多個tomcat進程的場景,因此每個tomcat jvm均檢測一遍

// 應對存在多個 tomcat 進程的情況
public static void attach(String agent_jar_path) throws Exception {
VirtualMachine virtualMachine = null
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
if (descriptor.displayName().contains("catalina") || descriptor.displayName().equals("")) {
try {
virtualMachine = VirtualMachine.attach(descriptor);
Properties targetSystemProperties = virtualMachine.getSystemProperties();
if (descriptor.displayName().equals("") && !targetSystemProperties.containsKey("catalina.home"))
continue;
// 將當前tomcat descriptor,傳到agent,作為檢測結果的文件名。也是用來區分多個tomcat進程。
String currentJvmName = "tomcat_" + descriptor.id();
Thread.sleep(1000);
javaInfoWarning(targetSystemProperties);
virtualMachine.loadAgent(agent_jar_path, currentJvmName);
} catch (Throwable t) {
t.printStackTrace();
} finally {
// detach
if (null != virtualMachine)
virtualMachine.detach();
               }
           }
       }
   }

3.遍歷tomcat jvm 加載過的類

  private static synchronized void detectMemShell(String currentJvmName, Instrumentation ins) {
       // 獲取所有加載的類
       Class<?>[] loadedClasses = ins.getAllLoadedClasses();
   }

四、風險類識別

最理想的做法是把所有加載的類都認定為風險類。但在絕大多數情況下jvm加載的都是正常的類,每次檢查時,都dump所有加載的類,對于tomcat(用戶側)來說開銷較大。

比較實際的做法是,根據已知內存馬要新增/修改的類生成特征。

對于內存中的每一個類,遞歸檢查其父類,然后將命中特征的類標記為風險類。

public static List<Class<?>> findAllSuspiciousClass (Instrumentation ins, Class<?>[] loadedClasses){
    // 結果
    List<Class<?>> suspiciousClassList = new ArrayList<Class<?>>();
    List<String> loadedClassesNames = new ArrayList<String>();
    // 獲取所有風險類
    for (Class<?> clazz : loadedClasses) {
        loadedClassesNames.add(clazz.getName());
        // 遞歸 檢查class的父類 空或java.lang.Object退出
        while (clazz != null && !clazz.getName().equals("java.lang.Object")) {
            if (
                    ClassUtils.lsContainRiskPackage(clazz) ||
                            ClassUtils.isUseAnnotations(clazz) ||
                            ClassUtils.lsHasRiskSuperClass(clazz) ||
                            ClassUtils.lsRiskClassName(clazz) ||
                            ClassUtils.lsReleaseRiskInterfaces(clazz)
            ){
                if (loadedClassesNames.contains(clazz.getName())) {
                    suspiciousClassList.add(clazz);
                    ClassUtils.dumpClass(ins, clazz.getName(), false,
                            Integer.toHexString(clazz.getClassLoader().hashCode()));
                    break;
                }
                LogUtils.logToFile("cannot find " + clazz.getName() + " classes in instrumentation");
                break;
            }
            clazz = clazz.getSuperclass();
        }
    }
    return suspiciousClassList;
}

這里借鑒了LandGrey師傅的黑名單,將內存馬的目標類的類名、繼承類、實現類、所屬的包、使用的注解均設置黑名單。

1. 實現類黑名單

檢測類是否實現javax.servlet.Filter / javax.servlet.Servlet / javax.servlet.ServletRequestListener接口類。

// 檢測類是否實現高風險接口,如servlet/filter/Listener
public static Boolean lsReleaseRiskInterfaces(Class<?> clazz){
    // 高風險的接口
    List<String> riskInterface = new ArrayList<String>();
    // filter型
    riskInterface.add("javax.servlet.Filter");
    // servlet型
    riskInterface.add("javax.servlet.Servlet");
    // listener型
    riskInterface.add("javax.servlet.ServletRequestListener");
    try {
        // 獲取類實現的interface
        List<String> clazzInterfaces = new ArrayList<String>();
        for (Class<?> cls : clazz.getInterfaces())
            clazzInterfaces.add(cls.getName());
        // 兩個list有交集 返回true
        clazzInterfaces.retainAll(riskInterface);
        if(clazzInterfaces.size()>0){
            return Boolean.TRUE;
        }
    } catch (Throwable ignored) {}
    return Boolean.FALSE;
}

2. 繼承類黑名單

// 檢測父類是否屬于高風險
public static Boolean lsHasRiskSuperClass(Class<?> clazz) {
    // 高風險的父類
    List<String> riskSuperClassesName = new ArrayList<String>();
    riskSuperClassesName.add("javax.servlet.http.HttpServlet");
    try {
        if ((clazz.getSuperclass() != null
                && riskSuperClassesName.contains(clazz.getSuperclass().getName())
        )){
            return Boolean.TRUE;
        }
    }catch (Throwable ignored) {}
    return Boolean.FALSE;
}

3. 注解黑名單

通過clazz.getDeclaredAnnotations() 獲取所有注解,如果類使用了spring注冊路由的注解,則標記為高風險。

public static Boolean isUseAnnotations(Class<?> clazz) {
    // 針對spring注冊路由的一些注解
    List<String> riskAnnotations = new ArrayList<String>();
    riskAnnotations.add("org.springframework.stereotype.Controller");
    riskAnnotations.add("org.springframework.web.bind.annotation.RestController");
    riskAnnotations.add("org.springframework.web.bind.annotation.RequestMapping");
    riskAnnotations.add("org.springframework.web.bind.annotation.GetMapping");
    riskAnnotations.add("org.springframework.web.bind.annotation.PostMapping");
    riskAnnotations.add("org.springframework.web.bind.annotation.PatchMapping");
    riskAnnotations.add("org.springframework.web.bind.annotation.PutMapping");
    riskAnnotations.add("org.springframework.web.bind.annotation.Mapping");
    try {
        // 獲取所有注解
        Annotation[] da = clazz.getDeclaredAnnotations();
        if (da.length > 0)
            for (Annotation _da : da) {
                // 比較 注解 && 高風險注解 如果有交集 返回True
                for (String _annotation : riskAnnotations) {
                    if (_da.annotationType().getName().equals(_annotation))
                        return Boolean.TRUE;
                }
            }
    } catch (Throwable ignored) {}
    return Boolean.FALSE;
}

4. 類名黑名單

// 高風險的類名
public static Boolean lsRiskClassName(Class<?> clazz){
    List<String> riskClassName = new ArrayList<String>();
    riskClassName.add("org.springframework.web.servlet.handler.AbstractHandlerMapping");
    try {
        if (riskClassName.contains(clazz.getName())){
            return Boolean.TRUE;
        }
    }catch (Throwable ignored) {}
    return Boolean.FALSE;
}

5. 包名黑名單

// 檢測是否屬于高風險的包
public static Boolean lsContainRiskPackage(Class<?> clazz){
    // 高風險的包
    List<String> riskPackage = new ArrayList<String>();
    riskPackage.add("net.rebeyond.");
    riskPackage.add("com.metasploit.");
    try {
        for (String packageName : riskPackage) {
            if (clazz.getName().startsWith(packageName)) {
                return Boolean.TRUE;
            }
        }
    }catch (Throwable ignored) {}
    return Boolean.FALSE;
}

6. 基于mbean的filter/servlet風險類識別

這里分享另一種filter/servlet的檢測,檢測思路是通過mbean獲取sevlet/filter列表,內存馬的filter是動態注冊的,所以web.xml中肯定沒有相應配置,因此通過對比可以發現異常的filter。

MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
Object mbsInte = getFieldValue(mbs, "mbsInterceptor");
Object repository = getFieldValue(mbsInte, "repository");
Object domainTb = getFieldValue(repository, "domainTb");
Map<String, Object> catlina = (Map<String, Object>)((Map<String,Object>)domainTb).get("Catalina");
for (Map.Entry<String, Object> entry : catlina.entrySet()) {
  String key = entry.getKey();
  // servlet
  if (key.contains("j2eeType=Servlet")){...}
  // filter 
  if (key.contains("j2eeType=Servlet") && key.contains("name=jsp")){
    Object value = entry.getValue();
    Object obj = getFieldValue(value,"object");
    Object res = getResourceValue(obj);
    Object instance = getFieldValue(res,"instance");
    Object rctxt = getFieldValue(instance, "rctxt");
    Object context = getFieldValue(instance, "context");
    Object appContext = getFieldValue(context,"context");
    Object standardContext = getFieldValue(appContext,"context");
    Object filterConfigs = getFieldValue(standardContext,"filterConfigs");
    ...

不過這種方式有較大的缺陷。首先,mbean只是資源管理,并不影響功能,所以在植入內存馬后再卸載掉注冊的mbean即可繞過;其次,servlet 3.0引入了 @WebFilter 可以動態注冊,這種也沒有在web.xml中配置,會引起誤報,因此僅可作為一個查找風險類參考條件。

五、檢測是否為內存馬

遍歷風險類,并檢測以下規則:

1.內存馬,對應的ClassLoader目錄下沒有對應的class文件

   public static Boolean checkClassIsNotExists(Class<?> clazz){
       String className = clazz.getName();
       String classNamePath = className.replace(".","/") + ".class";
       URL isExists = clazz.getClassLoader().getResource(classNamePath);
       if (isExists == null){
           return Boolean.TRUE;
       }
       return Boolean.FALSE;
   }

2.反編譯該類的字節碼,檢查是否存在危險函數

   public static Boolean checkFileContentIsRisk(File dumpPath){
       List<String> riskKeyword = new ArrayList<String>();
       riskKeyword.add("javax.crypto.");
       riskKeyword.add("ProcessBuilder");
       riskKeyword.add("getRuntime");
       riskKeyword.add("ProcessImpl");
       riskKeyword.add("shell");
       String content = PathUtils.getFileContent(dumpPath);
       for (String keyword : riskKeyword) {
           if (content.contains(keyword)) {
               return Boolean.TRUE;
           }
       }

結果輸出參考:如果沒有class文件,可將該類風險等級標為high。如果包含惡意代碼,將該類風險等級調至最高級。

   // 輸出結果
   public static String getClassRiskLevel(Class<?> clazz, File dumpPath) {
       String riskLevel = "Low";
       // 檢測 Classloader目錄下是否存在class文件
       if (AnalysisUtils.checkClassIsNotExists(clazz)){
           riskLevel = "high";
       }
       // 反編譯  檢測java文件是否包含執行命令的危險函數
       if (AnalysisUtils.checkFileContentIsRisk(dumpPath)){
           riskLevel = "Absolutely";
       }
       return riskLevel;
   }

六、小結

本文只是對Tomcat內存馬的檢測提供了一些思路,但并未提及查殺,查殺將在下一篇分享。

以上所有方法的黑名單列表僅供參考,可自行更改。

感謝 fnmsd、c0ny1、LandGrey 師傅們的支持。

七、參考文章


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1381/