Java安全

Java安全

java通过以下技术来实现安全机制

  • 语言设计特性(对数组边界检查,只进行合法的类型转换,无指针算法)
  • 访问控制机制,用于控制代码能够执行的功能
  • 代码签名,java代码的作者能够用加密算法来表明java代码的作者

一.类加载器

虚拟机只加载程序需要的类文件,例如程序从MyProgram.Class执行,下面是虚拟机执行步骤:

  1. 虚拟机有一个用于加载类文件的机制,例如从磁盘文件加载或web请求访问,使用该价值加载MyProgram.Class
  2. 如果MyProgram.Class拥有一个类型为另一个类的实例变量,或者拥有超类,那么这些类文件也被加载
  3. 接着虚拟机执行MyProgram.Class的main方法,因为它是静态,无需实例化
  4. 如果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
2
3
4
5
6
像 count++ 其实一行代码, 对应三个机器指令!
1)从内存读取数据到 CPU load
2)在 CPU 寄存器中,完成加法运算 add
3)把寄存器的数据写回到内存中 save
这几个步骤单线程下执行,没有任何问题
如果是多线程并发执行,这个就不一定了!!

当我们执行t1.start()、t2.start()后,t1线程和t2线程就在操作系统内核中创建出来了,t1、t2线程就参与到了系统调度当中

而调度是随机的——他可能先让t1执行几个指令,然后t2再执行几个指令、最后再把CPU的控制权交给t1。

于是因为系统的调度是随机的(这是罪魁祸首,但我们无法改变),当我们多个线程同时执行一些不是整体的操作的时候(++或–)由于并发就会产生一些问题

🌰栗子一

img

🌰栗子二

img

为什么会产生上面的BUG呢?

就是因为我们的++操作不是一个整体,是一个由多个指令所组成的操作

解决方案:也是加锁:“synchronized”,意味着把这三条指令打包成了一组指令,然后把这一组指令看出成一条指令了,类似于数学里的“整体代换”思想。

img

首先我们要明白加锁操作都是针对某一个对象来进行的(加锁本质就是给对象头里设置个标记),加锁有以下几种形式

形式一、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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( millis: 1000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("t1线程finish");
}
});
t1.start();

形式二、

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
2
3
4
5
6
public synchronized static void func1() {
}
public static void func2() {
synchronized(Counter.class){
}
}

使用类对象加锁,和类里有啥成员没关系~~
synchronized static、Counter.class这俩写法视为是等价的!!
类对象是整个程序里唯一的!!
这样加锁,但凡是调用到 func1 和 func2,之间都会产生竞争!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter1 {
public static int count;
public synchronized static void increase() {
++count;
}
public class demo77777
public static void main(String[] args) {
Counter1 counter1 = new Counter1();
Thread t1 = new Thread(() ->{
for (int i = 日; i < 500; i++) {
System.out.println("t1 start");
Counter1.increase();
System.out.printin("t1 finish");
}
});
t1.start();

当我们给不同的对象上锁后,如果用住房来比喻

不同的房间相当于是不同的对象,不同的线程相当于是不同的客人

如果房间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
2
3
4
5
6
7
8
9
import java.io.IOException;

public class Calc {
//当前执行命令无回显
public static void main(String[] args) throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
}

这里,在额外普及一下,如果需要回显怎么办的问题,主要是用IO流将命令执行后的字节加载出来,然后最基本的按行读取,就可以了。

在进行网站开发入JSP的时候,我们使用的JSP一句话木马也是根据这个原理进行编写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Ping {
//我们需要执行有回显得命令
public static void main(String[] args) throws IOException {
Process process = Runtime.getRuntime().exec("ping baidu.com");
InputStream inputStream = process.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader inputBufferedReader = new BufferedReader(inputStreamReader);
StringBuilder stringBuilder=new StringBuilder();
String line = null;
while ((line = inputBufferedReader.readLine()) != null) {
stringBuilder.append(line);
System.out.println(line);
}
inputBufferedReader.close();
inputBufferedReader=null;
inputStreamReader.close();
inputStreamReader=null;
inputStream.close();
inputStream=null;
}
}

Runtime的其他用法

我们在进行命令执行的时候,是需要区分操作系统的,不同的操作系统所执行的命令方式绝对是不一样的,

Windows下

windows 我们可以调用 cmd或者powershell去执行命令,但是powershell一般会限制执行策略,所以使用cmd一般是比较保险的

https://docs.microsoft.com/zh-cn/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.2

1
2
String [] cmd={"cmd","/C","calc.exe"}; 
Process proc =Runtime.getRuntime().exec(cmd);

linux下

对于linux的话,我们一般可以使用bash进行命令的执行,通常情况下是会有的,但是有的情况,可能没有bash,我们就可以使用sh来进行替代,

1
2
String [] cmd={"/bin/sh","-c","ls"}; 
Process proc =Runtime.getRuntime().exec(cmd);

于是乎,在后面我们写exp或者一些工具的时候,就需要根据主机的操作系统进行甄别

最简单的办法就是使用getProperty函数进行os的名称

1
System.getProperty("os.name");

初步demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Test {
public static void main(String[] args) throws IOException {
String property = System.getProperty("os.name");
String [] cmd1={"cmd","/C","start calc.exe"};
String [] cmd2={"/bin/sh","-c","ls"};
String [] cmd = null;
System.out.println(property);
if (property.contains("Windows")){
cmd= cmd1;
}
else {
cmd= cmd1;
}

Process process =Runtime.getRuntime().exec(cmd);
//取得命令结果的输出流
InputStream inputStream = process.getInputStream();
//用输出读取去读
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
//创建缓冲器
BufferedReader inputBufferedReader = new BufferedReader(inputStreamReader);
StringBuilder stringBuilder=new StringBuilder();
String line = null;
while ((line = inputBufferedReader.readLine()) != null) {
stringBuilder.append(line);
System.out.println(line);
}
inputBufferedReader.close();
inputBufferedReader=null;
inputStreamReader.close();
inputStreamReader=null;
inputStream.close();
inputStream=null;
// return stringBuilder;
// 这里如果要返回的值的话,返回的应该是stringBuilder
}
}

java反射的概念

Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

java有四个基本特征,封装,继承,多态,抽象

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。本质上其实就是动态的生成类似于上述的字节码,加载到jvm中运行

关键点:动态获取

反射获取对象

正常的new对象的过程如下,我们可以看到,首先编译出了Student的类,而Student.class 经过JVM的内存中,就始终存在一个(一个类只会存在一个class对象),

补一张jvm的图

img

双亲委派机制

1->2->3->4

由于java语言动态的特性,在程序运行后,所运行的类,就已经在JVM的内存中,我们就可以直接调用已经加载好的类去实现我们的方法操作。

在很大一部分情况下,公司上线的产品都是以jar包或者war包部署到Tomcat下运行的,我们如果有源码,能审查链以后,就可以直接利用加载在JVM中的类进行操作。

  1. Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
  2. Java属于==先编译再运行==的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
  3. 反射调用方法时,会忽略权限检查,可以无视权限修改对应的值—>因此容易导致安全性问题,(对安全研究人员来说提供了不小的帮助,hhhh)

获得实例化类的对象

主要是通过newInstance方法

方法一通过Class中的newInstance

1
2
3
4
Class p=Class.forName("Person");
Object p1=p.newInstance();
Class p=Class.forName("Person");
Person p1=(Person)p.newInstance();

方法二 Constructor中的newInstance

1
2
3
Class cla=p.getClass();
Constructor con = cla.getConstructor();
Object p1 = con.newInstance();

那么为什么要说这两种方法呢?如上第一中方法,只能适用于,我们的类只能含有不含参的构造器,一旦我们的类中没有不含参的构造器的时候第一中方法就会报错了

image.png

image.png

1
2
3
Class cla=Class.forName("phone");
Constructor con = cla.getConstructor(String.class);
Object p1 = con.newInstance("lituer");

image.png

Class.newInstance

newInstance,字面意思就是“新实例”,Class.newInstance是java反射中Class类创建新的实例化对象的方法,在这个过程中,是先取了这个类的不带参数的构造方法,然后调用构造方法 也就是无参构造函数 的 newInstance 来创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public T newInstance()
throws InstantiationException,
IllegalAccessException
Creates a new instance of the class represented by this Class object. The class is instantiated as if by a new expression with an empty argument list. The class is initialized if it has not already been initialized.
Note that this method propagates any exception thrown by the nullary constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed by the compiler. The Constructor.newInstance method avoids this problem by wrapping any exception thrown by the constructor in a (checked) InvocationTargetException.

Returns:
a newly allocated instance of the class represented by this object.
Throws:
IllegalAccessException - if the class or its nullary constructor is not accessible.
InstantiationException - if this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
ExceptionInInitializerError - if the initialization provoked by this method fails.
SecurityException - If a security manager, s, is present and any of the following conditions is met:
invocation of s.checkMemberAccess(this, Member.PUBLIC) denies creation of new instances of this class
the caller's class loader is not the same as or an ancestor of the class loader for the current class and invocation of s.checkPackageAccess() denies access to the package of this class

和我们上面说的一样,既然类中存在无参构造函数,那么面对有参构造函数,那么我们就不能使用Class.newInstance而是要用Constructor.newInstance()

然而,通过 Constructor.newInstance() 方法可以根据提供的参数调用任何构造函数。相比之下,Class.newInstance() 方法只能调用可见的构造函数,也就是公共(public)类型的构造函数。然而,在特定情况下,Constructor.newInstance() 方法可以调用私有的构造函数,需要使用 setAccessible(true) 方法来实现。

image.png

1
2
3
4
5
6
Class cla=Class.forName("MyClass");
Constructor constructor = cla.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Object obj=constructor.newInstance("lituer");
Method method = cla.getMethod("test");
method.invoke(obj);

image.png

执行成功。

获取类的构造器

一:获取public类型的构造器

1
2
getConstructor(class[]parameterTypes)//里面的参数可选
getConstructors()//获得类中所有public类型的构造器

实例:就上面用到的

1
2
3
4
Class cla=Class.forName("phone");
Constructor con = cla.getConstructor(String.class);//获得有参数的
Constructor con = cla.getConstructor();//获得无参数的
Constructor[] con = cla.getConstructors();//获得类中所有public类型的构造器,因为所有的构造器肯定不是一个,所以要用数组

image.png

二:getDeclaredConstructor(class[]parameterTypes)

它可以同时获取public和private以及protected还有默认类型的构造器

1
2
Class cla=Class.forName("phone");
Constructor con2 = cla.getDeclaredConstructor();

getDeclaredConstructors()方法全部的构造器

1
Constructor[] con2 = cla.getDeclaredConstructor();

获取类的属性

1,获得类的public类型的属性

1
2
3
4
5
getField(String name)
getFields()//全部public
Class cla=Class.forName("MyClass");
Field field = cla.getField("x");
Field[] field = cla.getFields();

2,获得任一一个类型属性

1
2
3
4
5
getDeclaredField(String name)
getDeclaredFields();//全部的任何类型的属性
Class cla=Class.forName("MyClass");
Field field = cla.getDeclaredField("y");
Field[] field = cla.getDeclaredFields();

获取类的方法

1,获得public类型的方法

1
2
3
4
5
getMethod(String name,class[] parameterTypes);//第一个参数是方法名,第二个参数是形参的类型,没有即不填
getMethods()
Class cla=Class.forName("MyClass");
Method method = cla.getMethod("test");
Method[] methods = cla.getMethods();

2,获得所有类型的方法

1
2
3
4
5
getDeclaredMethod(String name,class[] parameterTypes)
getDeclaredMethods()//全部方法
Class cla=Class.forName("MyClass");
Method method = cla.getDeclaredMethod("input", String.class);
Method[] methods=cla.getDeclaredMethods();

![image.png](https://storage.tttang.com/media/attachment/2023/08/17/51c6fabd-a4d5-45a0-b8b0-f5b0345b348e.png)

获得Method信息包括

  1. getName() 返回方法的名称
  2. getReturnType() 返回方法返回值类型 也是一个 Class实例比如 String.class
  3. getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class};
  4. 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
2
3
4
5
Class cla=Class.forName("MyClass");//获得Class对象
Constructor constructor =cla.getConstructor(String.class);//获得构造器
Object obj=constructor.newInstance("lituer");//实例化对象
Method method = cla.getMethod("test");//获得方法
method.invoke(obj);//执行方法

获得类属性的修改

我们使用set方法

image.png

我们当修改我们类中的属性可以通过

1
2
Class cla=Class.forName("MyClass");
Field field = cla.getField("x");

先获得属性,然后通过set来修改

1
2
3
Class cla=Class.forName("MyClass");
Field field = cla.getField("x");
field.set(p,"xiaoming");//这个p是Class类的实例对象

如果说是私有属性 的话我们怎么修改

第一步我们需要获得这个属性我们使用getDeclaredField

1
2
Class cla=Class.forName("MyClass");
Field field = cla.getDeclaredField("y");

然后我们还需要setAccessible

1
2
3
4
Class cla=Class.forName("MyClass");
Field field = cla.getDeclaredField("y");
field.setAccessible(true);
field.set(p,"xiaoming");