Java 8 中使用反射进行命令执行的 5 个方法

今天在逛先知的时候看到了一篇文章:《探究使用反射进行除Runtime的命令执行方法》https://xz.aliyun.com/t/12446 其中大概讲了下命令执行的其他构造方式,但最后没有给出实例,所以我这里就简单研究了一下。

概述

在RASP等安全产品防护严密的现在,普通的寻找Runtime.getRuntime().exec(cmds)的调用已经成为了一件不现实的事情
同样的,在Java中盛行的反序列化漏洞中,如果将RCE的功能简单的通过Runtime.getRuntime().exec(cmds)这种结构来进行实现可能大概率也不能达到我们的目的,所以探索一下Runtime的底层实现,使用更加底层且复杂的调用来进行RCE功能的实现相对来说更加的可行

命令执行

文章接下里要用到反射技术,这里就不细讲了,可以看看其他的文章学习一下:https://www.yuque.com/sanqiushu-dsz56/efe3vx/hchx1p

命令执行 1:Runtime.getRuntime().exec()

// 命令执行 1 Runtime.getRuntime().exec()
Runtime.getRuntime().exec("open -a Calculator");

这个是大家所熟知的 java 中执行外部命令的方法。
首先我们跟踪Runtime执行命令的过程
image
exec() 函数在这里接收一个String类型的参数,调用 exec() 的另一个重载方法对参数进行处理,将其通过分隔符(默认为空格),将其封装成了数组对象
image
然后继续调用 exec() 重载方法,执行字符串数组类型的命令。
image
然后通过 ProcessBuilder()....start() 去执行命令了。那么我们自己也是可以通过反射去手动调用这个方法的,构造一下。

命令执行 2:ProcessBuilder().start()

// 命令执行 2 ProcessBuilder().start()
String[] cmd_array = new String[] {"open","-a","Calculator"};
Process process = new ProcessBuilder(cmd_array).start();

// 或者
new ProcessBuilder("calc.exe").start();  // 不能有参数

//当然还可以有其他的变体(通过反射):
Class pro = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) pro.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

Class pro = Class.forName("java.lang.ProcessBuilder");
pro.getMethod("start").invoke(pro.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

Class pro = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) pro.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

Class pro = Class.forName("java.lang.ProcessBuilder");
pro.getMethod("start").invoke(pro.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));

environment() 是设置环境遍历的函数,directory() 是设置执行目录的函数,然而我们都没传值,所以直接可以忽略。继续看 start() 函数
image

继续跟进,系统先进行了一番检查,然后新建了一个 SecurityManager 进行检测命令是否允许,但默认情况下 SecurityManager 是 null ,不进行检查。

image

然后就发现了系统调用的是 ProcessImpl.start() 进行命令执行。
image

那么到这里我们又可以自己构造了

命令执行 3 ProcessImpl.start()

// 命令执行 3 ProcessImpl.start()
String[] cmd_array = new String[] {"open","-a","Calculator"};
Method method_start = Class.forName("java.lang.ProcessImpl").getDeclaredMethod("start",String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method_start.setAccessible(true);
method_start.invoke(null, cmd_array, null, null, null, false);  // 静态方法不需要传入实例

继续跟踪 ProcessImpl.start() 方法,在经过一番准备之后,系统来到了 UNIXProcess() 构造方法(我是 linux 系统)(Windows 系统是调用 create() 方法)
image

那么是不是意味着我们又可以构造了

命令执行 4 UNIXProcess()

// 命令执行 4 UNIXProcess()
String[] cmd_array = new String[] {"open","-a","Calculator"};
Class<?> clazz = Class.forName("java.lang.UNIXProcess");
Method method_toCString = clazz.getDeclaredMethod("toCString", String.class);
method_toCString.setAccessible(true);
Constructor<?> constructor = clazz.getDeclaredConstructor(byte[].class, byte[].class, int.class, byte[].class,
        int.class, byte[].class, int[].class, boolean.class);
constructor.setAccessible(true);
constructor.newInstance((byte[])method_toCString.invoke(null, cmd_array[0]),
        new byte[] {45, 97, 0, 67, 97, 108, 99, 117, 108, 97, 116, 111, 114, 0} , 2, null, 0, null,
        new int[]{-1,-1,-1}, false);

image

那么继续往下看 UNIXProcess() 构造函数里面的代码
image

这里就一句话,调用 forkAndExec() 函数,那么我们可以手动调用这个函数,开始构造

命令执行 5: forkAndExec()

命令执行 5: forkAndExec()
String[] cmd_array = new String[] {"ls"};
Class<?> clazz = Class.forName("java.lang.UNIXProcess");
Method method_toCString = clazz.getDeclaredMethod("toCString", String.class);
method_toCString.setAccessible(true);
Constructor<?> constructor = clazz.getDeclaredConstructor(byte[].class, byte[].class, int.class, byte[].class,
        int.class, byte[].class, int[].class, boolean.class);
constructor.setAccessible(true);
Method method_forkAndExec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class,
        byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
method_forkAndExec.setAccessible(true);
Object o = constructor.newInstance((byte[])method_toCString.invoke(null, cmd_array[0]),
        new byte[] {} , 0, null, 0, null, new int[]{-1,-1,-1}, false);
int pid = (int)method_forkAndExec.invoke(o, 2, new byte[]{47, 76, 105, 98, 114, 97, 114, 121, 47, 74,
                97, 118, 97, 47, 74, 97, 118, 97, 86, 105, 114, 116, 117, 97, 108, 77, 97, 99, 104, 105, 110,
                101, 115, 47, 106, 100, 107, 49, 46, 56, 46, 48, 95, 49, 48, 49, 46, 106, 100, 107, 47, 67,
                111, 110, 116, 101, 110, 116, 115, 47, 72, 111, 109, 101, 47, 106, 114, 101, 47, 108, 105,
                98, 47, 106, 115, 112, 97, 119, 110, 104, 101, 108, 112, 101, 114, 0},
        new byte[]{111, 112, 101, 110, 0},
        new byte[]{45, 97, 0, 67, 97, 108, 99, 117, 108, 97, 116, 111, 114, 0}, 2, null, 0, null,
        new int[]{-1, -1, -1}, false);

不过这个有点牵强,因为需要先构造一个 UNIXProcess 的实例才行。
当然,我这传入的都是 byte[] ,只能用于验证,后续需要对照源码,将每个参数的生成给自动化才行。

Windows 的最后面的代码执行途径不太一样,但是同理构造即可。

posted @ 2023-04-20 10:00  Nestar  阅读(1413)  评论(0编辑  收藏  举报