Tomcat 內存馬檢測
Java內存馬簡介
關于JAVA內存馬的發展歷史,這里引用下 c0ny1師傅的總結 。早在17年n1nty師傅的《Tomcat源碼調試筆記-看不見的shell》中已初見端倪,但一直不溫不火。后經過rebeyond師傅使用agent技術)加持后,拓展了內存馬的使用場景,然終停留在奇技淫巧上。在各類hw洗禮之后,文件shell明顯氣數已盡。內存馬以救命稻草的身份重回大眾視野。特別是今年在shiro的回顯研究之后,引發了無數安全研究員對內存webshell的研究,其中涌現出了LandGrey師傅構造的Spring controller內存馬。
從攻擊對象來說,可以將Java內存馬分為以下幾類:
1.servlet-api
filter型
servlet型
listener型
2.指定框架,如spring
3.字節碼增強型
4.任意JSP文件隱藏
為方便學習,webshell demo已整理至github。
整體思路
無論是以上哪種攻擊方式,影響的均為加載到tomcat jvm中的類。
從防守方的角度來說,可以通過java instrumentation機制,將檢測jar包attach到tomcat jvm,檢查加載到jvm中的類是否異常。
整體檢測思路為:
1.獲取tomcat jvm中所有加載的類
2.遍歷每個類,判斷是否為風險類。這里把可能被攻擊方新增/修改內存中的類,標記為風險類(比如實現了filter/servlet的類)
3.遍歷風險類,檢查是否為webshell:
檢查高風險類的class文件是否存在;
反編譯風險類字節碼,檢查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 pid,傳到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中配置,會引起誤報,因此僅可作為一個查找風險類的參考條件。
檢測是否為內存馬
遍歷風險類列表,并檢測以下規則:
內存馬,對應的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;
}
反編譯該類的字節碼,檢查是否存在危險函數
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內存馬的檢測提供了一些思路,但并未提及查殺,查殺將在下一篇詳細分享。
以上所有方法的黑名單列表僅供參考,可自行更改、擴充。