目录

Java 本地命令执行漏洞

Java 本地命令执行漏洞

背景

JDK 原生提供了本地系统命令执行的函数,攻击者可以通过该漏洞在目标服务器中执行任意系统命令。在 Java 中可用于执行系统命令的方式有 API 有:

  • java.lang.Runtime
  • java.lang.ProcessBuilder
  • java.lang.UNIXProcess/ProcessImpl

Runtime 命令执行

exec(String command)

在 Java 中通常会使用 java.lang.Runtime 类的 exec 方法来执行本地系统命令。

以如下程序执行命令为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.geekby;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws IOException {
        String cmd = "";
        Process p = Runtime.getRuntime().exec("ping 127.0.0.1" + cmd);
        InputStream fis = p.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(fis));
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }
}

上面的程序可以成功执行 ping 命令。假设现在攻击者可以控制 cmd 参数,通过命令拼接,去执行其它命令。以 cmd = ";pwd" 为例,可以发现,命令无法执行,甚至连 ping 命令都无法将结果回显。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131520032.png-water_print

为了探究命令执行失败的原因,首先跟踪程序调用栈:

1
2
3
4
5
6
7
8
create:-1, ProcessImpl (java.lang)
<init>:386, ProcessImpl (java.lang)
start:137, ProcessImpl (java.lang)
start:1029, ProcessBuilder (java.lang)
exec:620, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
main:8, Main (com.geekby)

通过跟进调用链可以发现,exec 方法最终调用到了一个重载函数 exec(String command, String[] envp, File dir) 中:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131512055.png-water_print

命令以字符串出入到该函数后,首先通过 StringTokenizer 对其进行了处理,根据 \t\n\r\f 把传入的 command 分割:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131527906.png-water_print

经过处理之后,最后实例化了 ProcessBuilder 来处理传入的 cmdarray。在此处也可以发现,Runtime.getRuntime.exec() 的底层实际上也是 ProcessBuilder。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131528937.png-water_print

继续跟进 ProcessBuilder 类中的 start 方法,在 该方法中将 cmdarry 第一个参数 cmdarry[0] 当作要执行的命令,把后面的 cmdarry[1:] 作为命令执行的参数转换成 byte 数组 argBlock。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131534214.png-water_print

此时 prog 是要执行的命令 ping, argBlock 都是传给 ping 的参数 127.0.0.1;pwd,经过 StringTokenizer 对字符串的处理,改变了命令执行的语义,无法将分号作为命令分隔符,进而实现命令注入。

exec(String cmdarray[])

Java Runtime 包中存在 exec 函数的重载函数,其参数类型为字符串数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.geekby;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws IOException {
        String cmd = ";pwd";
        Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
        InputStream fis = p.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(fis));
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }
}

跟进 exec 函数的底层代码,因为直接传入的是数组,所以没有经过 StringTokenizer 对字符串的处理

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131613486.png-water_print

最终跟进到 UNIXProcess 方法

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131617663.png-water_print

此时 prog 是要执行的命令 /bin/sh , argBlock 都是传给 ping 的参数 -c\x00"ping 127.0.0.1;pwd"

因此,在参数可控的情况下,不能采用命令分割的形式进行命令注入。根据具体情况,可以采取 base64 编码的形式。

load()

在 Java Runtime 包中,还有另一种加载外部库的形式去命令执行。通过加载动态链接库,如 linux 下的 so 文件,windows 下的 dll 文件。

1
msfvenom -p windows/x64/exec --platform win -a x64 CMD=calc.exe EXITFUNC=thread -f dll> calc.dll

测试代码:

1
2
3
4
5
6
public class RCE {
    public static void main(String[] args) {
        Runtime rt = Runtime.getRuntime();
        rt.load("D:\\calc.dll");
    }
}

ProcessBuilder

使用 ProcessBuilder 类创建一个进程,创建 ProcessBuilder 实例,指定该进程的名称和所需参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.geekby;

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        String cmd = ";pwd";
        ProcessBuilder pb = new ProcessBuilder("ping", "127.0.0.1", cmd);
        Process process = pb.start();
        InputStream fis = process.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(fis));
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }
}

调用栈:

1
2
3
4
5
create:-1, ProcessImpl (java.lang)
<init>:386, ProcessImpl (java.lang)
start:137, ProcessImpl (java.lang)
start:1029, ProcessBuilder (java.lang)
main:8, Main (com.geekby)

通过分析调用栈可以发现,ProcessBuilder 在底层调用的逻辑与 Runtime.getRuntime.exec 逻辑相似,在此不做赘述。

ProcessImpl

由于 ProcessImpl 的构造函数是 private 属性的,因此,需要用反射的方式调用其静态方法 start。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202131653031.png-water_print

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.geekby;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        Class clazz = Class.forName("java.lang.ProcessImpl");
        Method start = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
        start.setAccessible(true);
        start.invoke(null, (Object) new String[]{"open", "-a", "Calculator"}, null, null, null, false);
    }
}

调用栈:

1
2
3
4
5
6
7
8
create:-1, ProcessImpl (java.lang)
<init>:386, ProcessImpl (java.lang)
start:137, ProcessImpl (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
main:14, main (com.geekby)

防御

本地命令执行是一种非常高风险的漏洞,在任何时候都应当非常谨慎的使用,在业务中如果使用到了本地系统命令那么应当禁止接收用户传入参数。在很多时候攻击者会利用某些漏洞(如:Struts2、反序列化等)来攻击我们的业务系统,最终利用 Java 本地命令执行达到控制 Web 服务器的目的。这种情况下用户执行的系统命令对我们来说就不再受控制了,我们除了可以配置SecurityManager规则限制命令执行以外,还可以使用 RASP 来防御本地命令执行就显得更加的便捷可靠。

RASP 防御 Java 本地命令执行

在 Java 底层执行系统命令的 API 是 java.lang.UNIXProcess/ProcessImpl#forkAndExec 方法,forkAndExec 是一个 native 方法,如果想要 Hook 该方法需要使用 Agent 机制中的 Can-Set-Native-Method-Prefix,为 forkAndExec 设置一个别名,如:__RASP__forkAndExec,然后重写__RASP__forkAndExec 方法逻辑,即可实现对原 forkAndExec 方法 Hook。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * Hook Windows系统 ProcessImpl 类构造方法
 */
@RASPMethodHook(
      className = "java.lang.ProcessImpl", methodName = CONSTRUCTOR_INIT,
      methodArgsDesc = ".*", methodDescRegexp = true
)
public static class ProcessImplHook extends RASPMethodAdvice {

   @Override
   public RASPHookResult<?> onMethodEnter() {
      try {
         String[] commands = null;

         // JDK9+的API参数不一样!
         if (getArg(0) instanceof String[]) {
            commands = getArg(0);
         } else if (getArg(0) instanceof byte[]) {
            commands = new String[]{new String((byte[]) getArg(0))};
         }

         // 检测执行的命令合法性
         return LocalCommandHookHandler.processCommand(commands, getThisObject(), this);
      } catch (Exception e) {
         RASPLogger.log(AGENT_NAME + "处理ProcessImpl异常:" + e, e);
      }

      return new RASPHookResult<?>(RETURN);
   }

}