Java安全
Java安全
java通过以下技术来实现安全机制
- 语言设计特性(对数组边界检查,只进行合法的类型转换,无指针算法)
- 访问控制机制,用于控制代码能够执行的功能
- 代码签名,java代码的作者能够用加密算法来表明java代码的作者
一.类加载器
虚拟机只加载程序需要的类文件,例如程序从MyProgram.Class执行,下面是虚拟机执行步骤:
- 虚拟机有一个用于加载类文件的机制,例如从磁盘文件加载或web请求访问,使用该价值加载MyProgram.Class
- 如果MyProgram.Class拥有一个类型为另一个类的实例变量,或者拥有超类,那么这些类文件也被加载
- 接着虚拟机执行MyProgram.Class的main方法,因为它是静态,无需实例化
- 如果main方法或者调用其他方法使用到得类,这些类被加载
类加载机制至少拥有三个类加载器:
- 引导类加载器 负责加载系统类,从JAR文件rt.jar中加载,它是虚拟机整体一部分。而且它是C语言实现的,另外引导类加载器没有ClassLoader对象
- 扩展类加载器 用于从jre/lib/ext目录加载“标准的扩展”,
- 系统类加载器(也陈为应用类加载器) 加载应用类,它是在CLASSPATH环境变量或者是-classpath命令行选项设置的类目录里或者是JAR/ZIP文件里查找这些类。
Java线程安全
一、什么是线程安全
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的
🌰栗子
package Thread;
public class demo77 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start(); // 两个线程在创建好了后,线程所对应的PCB加入到系统链表,参与系统调度
t2.start();
// 让主线程main等t1、t2执行完了再接着往下走
t1.join();
t2.join();
System.out.println(count);
}
}
想这样,在多线程情况下,程序的运行结果不符合我们的预期,这被称为线程不安全
二、造成线程不安全的原因
根本原因:操作系统的随机调度执行,抢占式执行
还有:我们可以看到我们的count是一个全局变量,我们的线程t1、线程t2对count变量同时都进行了修改——++操作(为什么说是同时呢,因为我们的t1线程、t2线程在创建完了后就参与到系统调度,由系统随机分配线程的执行,可能是t2线程先执行10个指令然后t1再执行10个指令,相当于是同时)
那么我们就改一下代码让t1、t2分批次对count修改不就行了吗?
package Thread;
public class demo77 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
try {
t1.join();// 等t1线程执行完了,t2线程再执行
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start(); // 两个线程在创建好了后,线程所对应的PCB加入到系统链表,参与系统调度
t2.start();
t2.join(); // 等t2线程执行完了,主线程main再接着执行,执行顺序:t1->t2->打印count
System.out.println(count);
}
}
大家有没有想过为什么多个线程同时执行count++的时候就会出现BUG呢?
这是因为我们多个线程同时对同一变量修改的所造成的BUG往往和我们操作的原子性有关!!!这时候的操作往往不是一个整体,多个线程并发执行这些操作就可能出现一些问题
如果我们在变量修改过程中,操作是原子的——只是对应一个机器指令,那么即使是多个线程同时对同一个变量修改也不一定会造成BUG,但也可能造成BUG——要看具体的业务场景
总之我们要避免多个线程同时对同一个变量来操作
🍑 对原子性在多线程并发执行中出现问题的分析
🔔🔔注意:
1 | 像 count++ 其实一行代码, 对应三个机器指令! |
当我们执行t1.start()、t2.start()后,t1线程和t2线程就在操作系统内核中创建出来了,t1、t2线程就参与到了系统调度当中
而调度是随机的——他可能先让t1执行几个指令,然后t2再执行几个指令、最后再把CPU的控制权交给t1。
于是因为系统的调度是随机的(这是罪魁祸首,但我们无法改变),当我们多个线程同时执行一些不是整体的操作的时候(++或–)由于并发就会产生一些问题
🌰栗子一
🌰栗子二
为什么会产生上面的BUG呢?
就是因为我们的++操作不是一个整体,是一个由多个指令所组成的操作
解决方案:也是加锁:“synchronized”,意味着把这三条指令打包成了一组指令,然后把这一组指令看出成一条指令了,类似于数学里的“整体代换”思想。
首先我们要明白加锁操作都是针对某一个对象来进行的(加锁本质就是给对象头里设置个标记),加锁有以下几种形式
形式一、
1 | public class demo7777 { |
形式二、
package Thread;
class Counter {
public static int count;
// public synchronized void increase() {
// ++count; 这两种写法视为是等价的
// }
public void increase() {
synchronized (this) { // 这里this可以是任意对象,this可以有多个Counter counter1 = new Counter(), Counter counter2 = new Counter();
++count;
}
}
}
public class demo777 {
public static void main(String[] args) throws InterruptedException {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter1.increase();
}
// 多个线程去调用这个increase方法,其实就是针对这个Counter对象counter1来进行加锁
// 如果一个线程t1获取到了该对象counter1的锁,那么另一个线程t2就要等到counter1对应的锁开了后(t1线程执行完该锁里的内容——++操作)t2才能执行++操作
// 此时++操作相当于是成为了一个整体(相当于一个指令,当一个线程再执行这个加锁的整体的指令的时候,另一个线程只能阻塞等待)
});
t1.start();
t2.start();
t1.join();
t2.join(); // 确保线程t1和线程t2都执行完了,main主线程再接着执行——输出count
System.out.println(Counter.count); // 输出10000
}
}
形式三、
加锁本质 就是给对象头里设置个标记,它的成员对应的对象的对象头不受影响
1 | public synchronized static void func1() { |
使用类对象加锁,和类里有啥成员没关系~~
synchronized static、Counter.class这俩写法视为是等价的!!
类对象是整个程序里唯一的!!
这样加锁,但凡是调用到 func1 和 func2,之间都会产生竞争!!
1 | class Counter1 { |
当我们给不同的对象上锁后,如果用住房来比喻
不同的房间相当于是不同的对象,不同的线程相当于是不同的客人
如果房间1住了客人A,那么房间1就上了锁,客人B就需要等客人A不再住房间1(开了锁)然后客人B才能住房间1;或者客人B住其他的房间(其他的对象,没上锁的)
package Thread;
// 测试线程竞争,对锁的竞争
public class demo7777 {
public static Object object1 = new Object();
public static Object object2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 针对object1对象进行加锁,加锁操作是针对某一个对象来进行的
synchronized (object1) {
System.out.println("t1线程start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程finish");
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (object1) { // 针对object1对象来进行加锁操作
System.out.println("t2线程start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2线程finish");
}
});
t2.start();
}
}
无论是使用哪种用法,使用 synchronized 的时候都是要明确锁对象!(明确是对哪个对象加锁)
只有当两个线程针对同一个对象加锁的时候,才会发生竞争
如果是两个线程针对不同对象加锁,则没有竞争~
我们上面就是两个线程t1和t2同时对object1这个对象进行了加锁,然后t1与t2直接就产生了竞争。从上述代码的实现过程中我们也可以看到,等到t1线程执行完了后,t2线程才开始执行。
但如果是两个线程对不同的对象进行加锁,则没有竞争(就像两个客人(两个线程)住不同的房间(不同的对象)当然不会发生竞争。
package Thread;
// 测试线程竞争,对锁的竞争
public class demo7777 {
public static Object object1 = new Object();
public static Object object2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (object1) { // 针对object1对象来进行加锁,加锁操作是针对一个对象来进行的
System.out.println("t1线程start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程finish");
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (object2) { // 针对object2对象进行加锁
System.out.println("t2线程start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2线程finish");
}
});
t2.start();
}
}
两个线程可以做到同时执行
java安全入门(一)
首先,我们需要了解java得基本语法,没有了解得同学,建议学习完java得基本语法之后,再来学习
命令执行
首先介绍最基本的 java中的命令执行
我们以后,最常用的poc,就是下面的这条
1 | import java.io.IOException; |
这里,在额外普及一下,如果需要回显怎么办的问题,主要是用IO流将命令执行后的字节加载出来,然后最基本的按行读取,就可以了。
在进行网站开发入JSP的时候,我们使用的JSP一句话木马也是根据这个原理进行编写的。
1 | import java.io.BufferedReader; |
Runtime的其他用法
我们在进行命令执行的时候,是需要区分操作系统的,不同的操作系统所执行的命令方式绝对是不一样的,
Windows下
windows 我们可以调用 cmd或者powershell去执行命令,但是powershell一般会限制执行策略,所以使用cmd一般是比较保险的
1 | String [] cmd={"cmd","/C","calc.exe"}; |
linux下
对于linux的话,我们一般可以使用bash
进行命令的执行,通常情况下是会有的,但是有的情况,可能没有bash,我们就可以使用sh
来进行替代,
1 | String [] cmd={"/bin/sh","-c","ls"}; |
于是乎,在后面我们写exp或者一些工具的时候,就需要根据主机的操作系统进行甄别
最简单的办法就是使用getProperty
函数进行os
的名称
1 | System.getProperty("os.name"); |
初步demo
1 | import java.io.BufferedReader; |
java反射的概念
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
java有四个基本特征,封装,继承,多态,抽象
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。本质上其实就是动态的生成类似于上述的字节码,加载到jvm中运行
关键点:动态获取
反射获取对象
正常的new对象的过程如下,我们可以看到,首先编译出了Student的类,而Student.class 经过JVM的内存中,就始终存在一个(一个类只会存在一个class对象),
补一张jvm的图
双亲委派机制
1->2->3->4
由于java语言动态的特性,在程序运行后,所运行的类,就已经在JVM的内存中,我们就可以直接调用已经加载好的类去实现我们的方法操作。
在很大一部分情况下,公司上线的产品都是以jar包或者war包部署到Tomcat下运行的,我们如果有源码,能审查链以后,就可以直接利用加载在JVM中的类进行操作。
- Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
- Java属于==先编译再运行==的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
- 反射调用方法时,会忽略权限检查,可以无视权限修改对应的值—>因此容易导致安全性问题,(对安全研究人员来说提供了不小的帮助,hhhh)
获得实例化类的对象
主要是通过newInstance方法
方法一通过Class中的newInstance
1 | Class p=Class.forName("Person"); |
方法二 Constructor中的newInstance
1 | Class cla=p.getClass(); |
那么为什么要说这两种方法呢?如上第一中方法,只能适用于,我们的类只能含有不含参的构造器,一旦我们的类中没有不含参的构造器的时候第一中方法就会报错了
1 | Class cla=Class.forName("phone"); |
Class.newInstance
newInstance,字面意思就是“新实例”,Class.newInstance是java反射中Class类创建新的实例化对象的方法,在这个过程中,是先取了这个类的不带参数的构造方法,然后调用构造方法 也就是无参构造函数 的 newInstance 来创建对象
1 | public T newInstance() |
和我们上面说的一样,既然类中存在无参构造函数,那么面对有参构造函数,那么我们就不能使用Class.newInstance而是要用Constructor.newInstance()
然而,通过 Constructor.newInstance() 方法可以根据提供的参数调用任何构造函数。相比之下,Class.newInstance() 方法只能调用可见的构造函数,也就是公共(public)类型的构造函数。然而,在特定情况下,Constructor.newInstance() 方法可以调用私有的构造函数,需要使用 setAccessible(true) 方法来实现。
1 | Class cla=Class.forName("MyClass"); |
执行成功。
获取类的构造器
一:获取public类型的构造器
1 | getConstructor(class[]parameterTypes)//里面的参数可选 |
实例:就上面用到的
1 | Class cla=Class.forName("phone"); |
二:getDeclaredConstructor(class[]parameterTypes)
它可以同时获取public和private以及protected还有默认类型的构造器
1 | Class cla=Class.forName("phone"); |
getDeclaredConstructors()方法全部的构造器
1 | Constructor[] con2 = cla.getDeclaredConstructor(); |
获取类的属性
1,获得类的public类型的属性
1 | getField(String name) |
2,获得任一一个类型属性
1 | getDeclaredField(String name) |
获取类的方法
1,获得public类型的方法
1 | getMethod(String name,class[] parameterTypes);//第一个参数是方法名,第二个参数是形参的类型,没有即不填 |
2,获得所有类型的方法
1 | getDeclaredMethod(String name,class[] parameterTypes) |
获得Method信息包括
- getName() 返回方法的名称
- getReturnType() 返回方法返回值类型 也是一个 Class实例比如 String.class
- getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class};
- getModifiers():返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义
获得类方法的调用
使用invoke
1 | Object invoke(Object obj,Object...args) |
其中参数 obj 是实例化后的对象,args 为用于方法调用的参数,如果我们获取的方法为非静态方法,那么第一个参数是类对象,如果是一个静态方法,那么我第一个参数就是一个类
这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]…) ,其实在反射里就是 method.invoke([1], [2], [3], [4]…) 。
下面是整个反射利用的过程
1 | Class cla=Class.forName("MyClass");//获得Class对象 |
获得类属性的修改
我们使用set方法
我们当修改我们类中的属性可以通过
1 | Class cla=Class.forName("MyClass"); |
先获得属性,然后通过set来修改
1 | Class cla=Class.forName("MyClass"); |
如果说是私有属性 的话我们怎么修改
第一步我们需要获得这个属性我们使用getDeclaredField
1 | Class cla=Class.forName("MyClass"); |
然后我们还需要setAccessible
1 | Class cla=Class.forName("MyClass"); |