Java本地命令执行
关于 Java 命令执行,我们熟知的一般是使用 java.lang.Runtime 类的 exec 方法来执行本地系统命令。但实际上 exec() 并不是执行命令的终点,这点你我都心知肚明,最终一定是通过系统调用来实现,而这里我们要探讨其整个调用链过程,当目标过滤不当时,我们可以利用中间链子的方法以及反射来实现命令执行
Runtime 调用链
我们先用一个简单的一句话木马来测试
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
在 Runtime 类的 exec() 方法处下断点

由于 Runtime 类中定义了多个形参不同的 exec 方法,这里我们不考虑环境变量和工作目录,继续跟进,这里会有多个方法重载,我们跟进到最终的 public Process exec(String[] cmdarray, String[] envp, File dir)

这里出现了第一个链子 ProcessBuilder() 的 start() 方法,会根据当前 ProcessBuilder 实例中配置的属性(如命令、环境、工作目录、IO 重定向配置等)来创建一个新的操作系统进程

这些工作做好以后会进入 try 块,调用 ProcessImpl.start() 静态方法

继续跟进调用了 ProcessImpl() 私用方法,实例化 ProcessImpl 对象,这里开始准备调用 CreateProcess API,为其接收的数据做处理

后续跟进 native 方法 create()

到这里 Java 层就结束了,直接调用 Windows 系统函数 CreateProcess 创建进程
至此整个调用链为
1 | at java.lang.ProcessImpl.create(ProcessImpl.java:-1) |
在经典 Java 8 Unix/Linux 系统下 java.lang.ProcessImpl.start 之后应该是调用 UNIXProcess 构造方法,然后通过 native 方法 forkAndExec() 系统调用 fork -> exec


对于 JDK9 之后则是将 forkAndExec() 方法合并到了 ProcessImpl 中

反射 Runtime
现在我们就刚刚的一句话木马做一个反射版本,并对敏感字符串面量做处理,看起来像这样
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
ProcessBuilder 命令执行
其实只需要传递一个命令参数即可

1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
反射获取
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
ProcessImpl 命令执行
通过前面的调用链分析,我们依旧可以通过反射 ProcessImpl 来进行命令执行,在 UNIX/Linux 系统下只需要模拟其源码 C 语言风格的数据处理逻辑即可,Windows 系统会更简洁一些,但由于其私有属性,因此仅限于 JDK9 版本以下。简言之,我们现在需要重写 ProcessImpl 的 start() 方法
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
Unix/Linux 版本大同小异,对照源码更改即可
create()/forkAndExec() 配合 Unsafe 类实现命令执行
现在考虑如果 RASP 把 UNIXProcess/ProcessImpl 类的构造方法给拦截了,我们可以借助一个叫 sun.misc.Unsafe 的类避免使用其构造方法
sun.misc.Unsafe 是 JDK 9 以下内部提供的一个“后门”类,绕过了 Java 的安全机制,直接操作内存和线程。 Unsafe 的构造方法是私有的,getUnsafe() 方法会检查调用者的类加载器(默认只允许 Bootstrap Classloader 调用),普通代码直接调用会抛出 SecurityException。

所以只能通过反射拿到它:
1 | Field f = Unsafe.class.getDeclaredField("theUnsafe"); |
allocateInstance 方法创建类实例
这是一个 native 方法,直接在堆上为指定类分配内存并返回实例,但完全跳过构造方法的执行
1 | public native Object allocateInstance(Class<?> cls) throws InstantiationException; |
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
这里我们成功执行了命令,但是如何读取输出流呢?放心,在源码中已经给出了逻辑

stdHandles[1] 对应标准输出,并通过反射构建 FileDescriptor 的方法来读取输出,这里为了避免使用构造方法,我们依旧可以使用 unsafe.allocateInstance() 来创建实例
至此对于 Java 本地命令执行的剖析告一段落,在以后的代码审计中也不能只局限于关注 exec 字段了




