白袍的小行星

Java安全基础

字数统计: 2.1k阅读时长: 9 min
2020/07/30 Share

又是一个新坑,想学习一下Java安全,毕竟学了才是安全人上人。

反射

基础知识

在说反射之前,先说一下Java的Class,它像是mysql中的information_schema数据库,存储了类的类型信息,也就是你创建的每个类其实都是Class类的对象。

根据Class类的构造方法可以得知,只有JVM才可以创建此类,因为它是私有的:

private Class(ClassLoader loader) {
......
classLoader = loader;
......
}

所以我们不能显式地声明一个Class对象,但是肯定还有其他方法:

  1. 通过类的静态成员表示,如:

    //自定义一个MyClass类
    Class c = MyClass.class;
  2. 通过类对象的 getClass() 方法,实际上是Object类的方法,如:

    //实例化一个MyClass对象,为myclass
    Class c = myclass.getClass();
  3. 通过 Class 类的静态方法 forName() 方法获取 Class 的对象,如:

    //动态加载
    myclass = Class.forName("custom.TheClass");

如果反过来,能否从Class类的对象得到其他类的对象呢?答案是肯定的,来看如下代码:

public class test {
public static void main(String args[]) throws IllegalAccessException, InstantiationException {
Class myclass = MyClass.class;
MyClass myClass = (MyClass) myclass.newInstance();
System.out.println(myClass.a);
}
}

class MyClass{
//必须调用此类的无参数构造方法
public MyClass(){
}
public String a = "Adan0s";

}

反射机制

上面所说的其实就是反射了,即在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。

注意,这里的重点是运行时任意一个类,Java可以利用反射来加载在运行时才得知名称的class文件,并且生成其对象,调用其变量或方法。

只要看了基础知识部分的,我相信应该都能理解反射是怎么回事了,下面介绍几个反射中及其重要的方法:

  • 实例化类对象的方法:newInstance
  • 获取函数的方法:getMethod
  • 执行函数的方法:invoke

来一个p牛写的例子,加深理解:

public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}

这段代码就是运用反射机制,完全动态地调用某类的某方法。

实际上我们要想利用反射来实现任意对象的任意方法调用,主要分三步:

  1. 首先获得这个对象对应的Class类的实例
  2. 通过Class类的实例来获取想要调用的那个方法
  3. 传入相应参数调用

分段来演示每一部分,先来写一个类:

class MyClass{
public MyClass(){}

public String a = "Public a";
private String b = "Private b";

public void getN(){
System.out.println("This is Public getN");
}
private void getM(){
System.out.println("This is Private getM");
}
}
  • 获取此类的所有变量名:
Class testclass = MyClass.class;
Field[] fields = testclass.getDeclaredFields(); //不分权限,获取类的成员变量名
//getFields() 此类以及其所继承的父类的public变量

for(Field field : fields){
System.out.println(field);
}

  • 获取此类的所有方法
Class testclass = MyClass.class;
Method[] methods = testclass.getDeclaredMethods(); //不分权限,获取类的所有成员方法
//getMethods() 获取类所有 public 访问权限的方法,包括从父类继承的
for(Method method : methods){
int modifiers = method.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
Class returnType = method.getReturnType();
System.out.print(returnType.getName() + " " + method.getName() + "( ");
Parameter[] parameters = method.getParameters();
for (Parameter parameter: parameters) {
System.out.print(parameter.getType().getName() + " " + parameter.getName() + ",");
}
System.out.println(" )");

虽然我们可以得到类的变量和方法信息,但是还有个问题:我们还没法修改私有变量,也没法调用私有方法。

其实是可以的,我们先修改一下MyClass类里的私有方法,加上参数:

private void getM(String name){
System.out.println("This is Private getM, name is "+ name);
}
  • 访问类的私有方法
Class testclass = MyClass.class;

Method getM = testclass.getDeclaredMethod("getM", String.class);//获取私有方法
getM.setAccessible(true);//获取私有方法的访问权
getM.invoke(classtest,"adan0s");

  • 修改类的私有变量

因为私有变量不能从外部获得,所以我们需要给MyClass类加上一个方法:

public void getB(){
System.out.println(b);
}

代码如下:

Class testclass = MyClass.class;
MyClass classtest = (MyClass) testclass.newInstance();

Field privateField = testclass.getDeclaredField("b");//获取私有变量
privateField.setAccessible(true);//获取私有变量的访问权
System.out.print("修改前:");
classtest.getB();
privateField.set(classtest, "Private b Modified");
System.out.print("修改后:");
classtest.getB();

来点真实的

现在来尝试一下使用反射来执行命令,最常使用到的类自然是:java.lang.Runtime.

写出调用代码:

Class run = Class.forName("java.lang.Runtime");
Method execName = run.getMethod("exec", String.class);
execName.invoke(run.newInstance(), "whoami");

运行发现报错,原因是Runtime的构造方法是私有的,这时我们有两种解决方法

  1. 这种构造方法为私有的,肯定会有静态方法去调用它,所以使用静态方法来实现
  2. 调用其私有的构造方法

第一种,其静态方法为getRuntime(),所以修改代码如下:

Class run = Class.forName("java.lang.Runtime");
Method runName = run.getMethod("exec", String.class);
Method execName = run.getMethod("getRuntime");
Object runtime = execName.invoke(runName);
System.out.println(runName.invoke(runtime, "whoami"));

第二种就不演示了,之前代码中已经包含了。

反序列化

基础知识

关于什么是反序列化,不多谈,了解过PHP反序列化的都懂。

在Java中,一个类对象想实现序列化,需要:

  1. 该类必须实现 java.io.Serializable 对象
  2. 该类的所有属性必须是可序列化的(此处有例外)

还是通过代码来演示,我们先写一个Person类来用作序列化的内容。

class Person implements Serializable{
private String name;
private Integer age;

public Person(String name, Integer age){
this.name = name;
this.age = age;
}

public void printInfo(){
System.out.println("My name is " + this.name + ", I'm " + this.age + " years old.");
}
}

序列化Person的对象并写入文件:

Person p = new Person("adan0s", 20);
try{
FileOutputStream fileOut = new FileOutputStream("./person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(p);
out.close();
fileOut.close();
}catch (IOException i){
i.printStackTrace();
}

这里主要是利用ObjectOutputStream流来把对象进行包装,再使用FileOutputStream流写入文件。

下面是反序列化:

Person p = null;
try{
FileInputStream fileIn = new FileInputStream("./person.ser");
ObjectInputStream input = new ObjectInputStream(fileIn);
p = (Person) input.readObject();
input.close();
fileIn.close();
p.printInfo();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

其实就是把序列化反着来了一遍。

漏洞利用

如同PHP一样,Java的反序列化漏洞也是靠自动触发的函数完成的,这个函数是readObject(),虽然它是ObjectInputStream的方法,但我们可以自己实现此函数,那么反序列化的时候就会调用我们实现的此函数了。

Person类里加上:

private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException{
System.out.println("此处可以执行命令");
}

然后序列化后再反序列化即可看到:

RMI

Java RMI(Remote Method Invocation)即Java远程方法调用,它允许一个 JVM 上的 object 调用另一个 JVM 上 object 方法。

RMI程序通常包括三部分:

  • RMI Registry,提供注册查询等功能,是一种特殊的Remote Object
  • RMI Server,创建Remote Object,将其注册到RMI Registry
  • RMI Client, 通过nameRMI Registry获取Remote Object reference (stub),调用其方法

下面动手尝试一下,编写一个完整的RMI程序,先主要有三部分:

  • 一个继承java.rmi.Remote的接口
  • 一个实现此接口的类
  • 一个用来创建Registry的主类,实例化后绑定地址

先实现一个接口,此接口要继承java.rmi.Remote,再声明一个测试函数在里面:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMIremote extends Remote {
public String rmiTest() throws RuntimeException, RemoteException;
}

再将这个接口实现:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIremoteTest extends UnicastRemoteObject implements RMIremote {
public RMIremoteTest() throws RemoteException {
super();
}

@Override
public String rmiTest() throws RuntimeException {
return "RMI 演示";
}
}

接下来编写服务端:

import implement.RMIremote;
import implement.RMIremoteTest;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RMIremote rmi = new RMIremoteTest();
Registry registry = LocateRegistry.createRegistry(1099);//绑定对象
registry.bind("rmi://127.0.0.1:1099/test", rmi);
}
}

最后是客户端:

import implement.RMIremote;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry(1099);
RMIremote rmi = (RMIremote) registry.lookup("rmi://127.0.0.1:1099/test");
System.out.println(rmi.rmiTest());
}
}

先启动服务端,再运行客户端:

演示环境是在同一台主机上的,如果不在同一台主机,需要保证接口在客户端与服务端都存在。

CATALOG
  1. 1. 反射
    1. 1.1. 基础知识
    2. 1.2. 反射机制
    3. 1.3. 来点真实的
  2. 2. 反序列化
    1. 2.1. 基础知识
    2. 2.2. 漏洞利用
  3. 3. RMI