Java安全入门(一)

命令执行

首先介绍最基本的 java中的命令执行

弹出计算器,最常用的poc,就是下面的这条

import java.io.IOException;

public class calc {
    public static void main(String[] args) throws IOException {
        java.lang.Runtime.getRuntime().exec("calc.exe");
    }
}

运行结果:

Java安全入门(一)_第1张图片

弹出计算器是没有回显的,如果需要回显怎么办的问题,主要是用IO流将命令执行后的字节加载出来,然后最基本的按行读取,就可以了。我们使用的JSP一句话木马也是根据这个原理进行编写的。

执行ping命令并回显结果,代码如下:

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;
    }
}

运行结果: 

Java安全入门(一)_第2张图片

Runtime的其他用法

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

Windows下

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

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

linux下

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

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

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

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

System.getProperty("os.name");

代码如下:

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
    }
}

运行结果:

打印出了windows 10

Java安全入门(一)_第3张图片

反射机制

什么是反射:

Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。

Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。

我们为什么必须使用反射

回想一下 java 中的基本操作,我们可以知道,类是对象的模板,对象是类的实例。一般我们都使用new的方式来创建一个对象,比如

Student stu1 = new Student();
//假设定义类之后,进行无参构造

那么,反射,为什么需要反射呢?

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

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

反射机制原理

借一张图,这张图应该很清楚的解释了它的原理,文章参考下面这篇写的很详细!

Java基础篇:反射机制详解_张维鹏的博客-CSDN博客_java反射机制原理详解

Java安全入门(一)_第4张图片

补一张JVM的图

Java安全入门(一)_第5张图片

反射的优缺点

优点:在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

缺点:反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;

反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

反射的用途

反编译:.class-->.java,通过反射机制访问java对象的属性,方法,构造方法等

反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。

我们最常见的反射举例,是加载数据库驱动时的

Class.forName("com.mysql.jdbc.Driver");//动态加载JDBC驱动
Connection conn = DriverManager.getConnection(url, user, password);

反射机制常用的类:


Java.lang.Class;

Java.lang.reflect.Constructor;

Java.lang.reflect.Field;

Java.lang.reflect.Method;

Java.lang.reflect.Modifier;

基本使用

获得Class

  • 类名.class,如:com.student.Student.class。

  • ClassLoader.getSystemClassLoader().loadClass("com.student.Student")

  • Class.forName("com.student.Student")

于是乎,我们通过反射可以这样获取Runtime

String className     = "java.lang.Runtime";
Class  runtimeClass1 = Class.forName(className);
Class  runtimeClass2 = java.lang.Runtime.class;
Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

获取构造器

这里,有两个方法

  • getDeclaredConstructor()

  • getConstructor()

其中,getDeclaredConstructor()可以获得构造方法,也就是我们常用的private方法,其中Runtime的构造方法是private,我们无法直接调用,我们需要使用反射去修改方法的访问权限(使用setAccessible,修改为 true)

Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);

通过获取的构造器进行实例化对象

// Object类是所有类的父类,有兴趣的同学可以在双亲委派机制中去搞明白
Object runtimeInstance = constructor.newInstance();
//这里的话就等价于
Runtime rt = new Runtime();

获取方法

Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

当我们想获取当前类的所有成员方法时们可以使用

Method[] methods = class.getDeclaredMethods()

获取当前类指定的成员方法时,

Method method = class.getDeclaredMethod("方法名");
Method method = class.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

执行方法

Process process = (Process) runtimeMethod.invoke(runtimeInstance, "calc");

这里简单解释一下 invoke方法

method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
  1. invoke就是调用类中的方法,最简单的用法是可以把方法参数化invoke(class, method)

这里则是使用了 class.invoke(method,"参数")的一个方式

  1. 还可以把方法名存进数组v[],然后循环里invoke(test,v[i]),就顺序调用了全部方法

(之后会介绍)

回显结果,不需要回显的话就忽略

        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();
        inputStreamReader.close();
        inputStream.close();

获取成员变量(Demo中没有用到)

反射还可以对成员变量进行操作

//获取类中的成员们变量
Field fields = class.getDeclaredFields();
//获取当前类指定的成员变量
Field field  = class.getDeclaredField("变量名");
//获取成员变量的值
Object obj = field.get(类实例对象);
//修改成员变量的值
field.set(类实例对象, 修改后的值);

Runtime实例:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RuntimeTest {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        Class  runtimeClass1 = Class.forName("java.lang.Runtime");//相当于 import
        Constructor constructor =  runtimeClass1.getDeclaredConstructor();//相当于编写无参构造类
        constructor.setAccessible(true);//取消private的限制
        Object runtimeInstance = constructor.newInstance();//有了无参构造类以后,我们new了一个对象
        Method method = runtimeClass1.getMethod("exec",String.class);//编写类的方法
        Process process = (Process) method.invoke(runtimeInstance,"calc");//调用了类的方法
        //接下来就是回显的结果
        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();
        inputStreamReader.close();
        inputStream.close();
    }
}

序列化和反序列化

序列化和反序列化的概念,把对象转换为字节序列的过程称为对象的序列化。
  把字节序列恢复为对象的过程称为对象的反序列化。
  对象的序列化主要有两种用途:
  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列。

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
 

序列化:

  • ObjectOutputStream类 -> writeObject()

    该⽅法对参数指定的obj⽂件进⾏序列化把字节序列写到⼀个⽬标输出流中,按照java标准是 给⽂件⼀个 ser 的扩展名

反序列化

  • ObjectInputStream类-> readObject()

    该⽅法是从⼀个输⼊流中读取字节序列,再把他们反序列化成对象,将其返回

Java反序列化时会执⾏readObject()⽅法,所以如果readObject()⽅法被恶意构造 的话,就有可能导致命令执⾏。

package com.Serializable;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
    
    private static final long serialVersionUID = 2632590740470689522L;
    private String name;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    private void readObject(ObjectInputStream in ) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Runtime.getRuntime().exec("calc.exe");
    }
}
package com.Serializable;

import java.io.*;

public class Test {
    public static void main(String args[]) throws Exception{
        User user = new User();
        //这里设不设置属性无所谓,主要看当时的情况
        user.setName("Wuming");
        //序列化
        OutputStream outputStream = new FileOutputStream(new File("C:\\Users\\12451\\Desktop\\Java-Learn\\src\\main\\java\\com\\Serializable\\test.ser"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(user);

        //反序列化
        InputStream inputStream = new FileInputStream(new File("C:\\Users\\12451\\Desktop\\Java-Learn\\src\\main\\java\\com\\Serializable\\test.ser"));
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        User test = (User) objectInputStream.readObject();

//    //把object对象储存为字节流的形式
//        FileOutputStream fos = new FileOutputStream("object");
//        ObjectOutputStream os = new ObjectOutputStream(fos);
//    //将对象写⼊object⽂件
//        os.writeObject(user);
//        os.close();
//    //从⽂件中反序列化obj对象
//        FileInputStream fis = new FileInputStream("object");
//        ObjectInputStream ois = new ObjectInputStream(fis);
//    //恢复对象
//        User user1 = (User) ois.readObject();
//        System.out.println(user1.getName());
//        ois.close();
    }
}

关于serialVersionUID(序列号)

当我们没有自定义序列化ID

如果我们没有自定义序列化id,当我们修改User 类的时候,编译器又为我们User 类生成了一个UID,而序列化和反序列化就是通过对比其SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。在实际的生产环境中,如果我们有需求要在序列化后添加一个字段或者方法,应该怎么办?那就是自己去指定serialVersionUID。

设置序列化ID

序列化运行时将一个版本号与每个称为SerialVersionUID的可序列化类相关联,在反序列化过程中使用该序列号验证序列化对象的发送方和接收方是否为该对象加载了与序列化兼容的类。如果接收方为对象加载的类的UID与相应发送方类的UID不同,则反序列化将导致InvalidClassException. 可序列化类可以通过声明字段名来显式声明自己的UID。

它必须是static、final和long类型。例如

(public/private/protected/default) static final long serialVersionUID=42L;

如果可序列化类没有显式声明serialVersionUID,则序列化运行时将根据类的各个方面为该类计算默认值,如Java对象序列化规范中所述。但是,强烈建议所有可序列化类显式声明serialVersionUID值,因为它的计算对类细节高度敏感,这些细节可能因编译器实现而异,因此类中的任何更改或使用不同的id都可能影响序列化的数据。

还建议对UID使用private修饰符,因为它作为继承成员没有用处。

IDEA设置自动生成UID的方式请参考使用IntelliJ IDEA自动生成serialVersionUID_蝈蝈的博客-CSDN博客_idea自动生成serialversionuid

你可能感兴趣的:(网络安全,学习笔记,java,安全,开发语言)