Java安全基础知识

语雀不充钱出不了网,纯纯跳板,不定时更新。

反射

概念

Java反射机制指的是:

  • 可以创建任意类的对象
  • 可以获取任意对象所属类
  • 可以访问任意类的,任意函数和成员

在Java安全里,我们通常利用这个来控制一些对象的成员、执行一些方法。

获取Class对象

获取Class对象通常是反射的第一步,class对象可以看作“类对象”,至于是哪个类,却决于你创建这个class对象用的哪个类。

  • 类名.class
  • 对象.getClass()
  • Class.forName(全类名)
  • ClassLoader.getSystemClassLoader().loadClass(全类名)

Class对象的方法

利用Class对象的内置方法,可以访问到一个类的各种部分。
Class clazz = Class.forName("com.example.demo.reflectdemo.UserInfo");

方法名 含义 运行结果
clazz.getPackage() 获取clazz所属类的包名 package com.example.demo.reflectdemo
clazz.getDeclaredAnnotations() 获取clazz所属类的所有注解
clazz.getModifiers() 获取clazz所属类的修饰符 public字符串
clazz.getName() 获取clazz所属类的全类名 com.example.demo.reflectdemo.UserInfo
clazz.getSimpleName() 获取clazz所属类的简单类名 UserInfo
clazz.getGenericSuperclass() 获取clazz所属类的超类 class java.lang.Object
clazz.getGenericInterfaces() 获取clazz所属类的实现接口 null
clazz.newInstance() 创建clazz所属类的实例 得到userInfo对象
clazz.getField("age") 获取clazz所属类/父类的Public的age属性 public int xx.xx.UserInfo.age
clazz.getFields() 获取clazz所属类/父类所有Public的属性
clazz.getDeclaredField("age") 获取clazz所属类的age属性,即使私有 private int xx.xx.UserInfo.age
clazz.getDeclaredFields() 获取clazz所属类的所有属性,即使私有
clazz.getMethod("setAge",String.class) 获取clazz所属类/父类的public的setAge方法 public void xx.xx.UserInfo.setName(java.lang.String)
clazz.getMethods() 获取clazz所属类/父类的public的所有方法
clazz.getDeclaredMethod("getName") 获取clazz所属类的getName方法,即使私有 public String xx.xx.UserInfo.getName()
clazz.getDeclaredMethods() 获取clazz所属类的所有方法,即使私有
method.getParameters() 获取传入method所属函数的所有参数 java.lang.String arg0
clazz.getConstructor(String.class,int.class) 获取一个声明为 public 构造函数实例 public xx.xx.UserInfo(java.lang.String,int)
clazz.getConstructors() 获取所有声明为 public 构造函数实例
clazz.getDeclaredConstructor(String.class) 获取一个声明的构造函数实例,即使私有 private xx.xx.UserInfo(java.lang.String)
clazz.getDeclaredConstructors() 获取所有声明的构造函数实例,即使私有
Object user = c1.newInstance("Jasper",22) 利用构造函数实例创建UserInfo实例 获得一个user对象,name=jasper,age=22
clazz.getModifiers() 获取clazz所属类的修饰符的值 1(public)
clazz.getDeclaredField("name").getModifiers() 获取属性的修饰符的值 2(private)
clazz.getDeclaredMethod("setName",String.class).getModifiers() 获取setName方法的修饰符的值 1(public)
Modifier.toString(1) 根据修饰符的值获取对应字符串 public(1)
method.invoke(object, args) 执行object的method方法,传入args参数 等价于object.method(args)

反射实现命令执行

java.lang.Runtime

java.lang.Runtime这个类的构造函数是私有的,所以不能直接newInstance()创建出一个runtime实例。
利用反射执行java.lang.Runtime.getRuntime().exec("calc");的有下面两种

1.通过getMethod

package com.example.demo.codeexec;
import java.lang.reflect.Method;

public class RuntimeGetMethod {
    public static void main(String[] args) throws Exception {
        //想要使用反射调用的方法如下:
        //java.lang.Runtime.getRuntime().exec("calc");

        //获取java.lang.Runtime的类对象
        Class<?> clazz = Class.forName("java.lang.Runtime");

        //获取函数对象
        Method getRuntime = clazz.getMethod("getRuntime");
        Method exec = clazz.getMethod("exec", String.class);

        //调用函数
        // 函数.invoke(函数所属对象, 参数) == 函数所属对象.函数(参数)
        Object runtime = getRuntime.invoke(clazz);
        exec.invoke(runtime,"calc");
    }
}

2.通过getDeclaredMethod

package com.example.demo.codeexec;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class RuntimeGetDeclaredConstructor {
    public static void main(String[] args) throws Exception {
        //获取类对象
        Class<?> clazz = Class.forName("java.lang.Runtime");
        //获取私有构造函数对象c
        Constructor c = clazz.getDeclaredConstructor();
        //设置私有构造函数对象可访问
        c.setAccessible(true);
        //创建出runtime对象
        Object runtime = (Runtime) c.newInstance();
        //获取exec函数对象
        Method execMethod = clazz.getMethod("exec", String.class);
        //invoke调用函数,runtime.exec("calc")
        execMethod.invoke(runtime,"calc");
    }
}

java.lang.ProcessBuilder

利用反射执行Process cmd = new ProcessBuilder(command).start();的方法如下:

package com.example.demo.codeexec;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class ProcessBuilderGetConstructor {
    public static void main(String[] args) throws Exception {
        // 获取到ProcessBuilder类对象
        Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
        //获取ProcessBuilder的构造函数
        Constructor c = clazz.getConstructor(List.class);
        // 创建ProcessBuilder实例,并传入初始参数
        Object processBuilder = c.newInstance(Arrays.asList("calc"));
        //获取start()函数对象
        Method start = clazz.getMethod("start");
        start.invoke(processBuilder);
    }
}

java.lang.ProcessImpl

这个类的命令执行就是反射实现的,代码如下:

package com.example.demo.codeexec;
import java.lang.reflect.Method;
import java.util.Map;

public class ProcessImplExec  {
    public static void main(String[] args)  throws Exception {
        //获取ProcessImpl的类对象
        Class clazz = Class.forName("java.lang.ProcessImpl");
        //获取start函数对象
        Method startMethod = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
        //start函数是static的,需要设置访问权限
        startMethod.setAccessible(true);
        //设置start的参数列表
        String[] cmds = new String[]{"calc"};
        //invoke调用,start(cmd)
        Process process = (Process) startMethod.invoke(null,cmds,null,".",null,true);
    }
}

反序列化

思想和php的序列化、反序列化一样,都是对象->字符->对象
注意几个点:

  • 要序列化的类,需要实现java.io.Serializable接口
  • 要序列化的类,里面的属性必须都可以序列化,不可以的需要声明是短暂的
  • 反序列化的方法是可以在类里自定义的
public class HackInfo implements java.io.Serializable{
    public String id;
    public String team;
    
    //这里重写一个readObject,用来自定义反序列化的时候干什么,弹个计算器
        private void readObject(java.io.ObjectInputStream in) throws Exception{
            Runtime.getRuntime().exec("calc");
    }
}

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeDemo {
    public static void main(String[] args) throws IOException {
        HackInfo hacker = new HackInfo();
        hacker.id = "Jasper";
        hacker.team = "星盟安全团队预备队";
    	//文件流转对象流
        FileOutputStream fos = new FileOutputStream("hacker.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //序列化执行writeObject()方法
        os.writeObject(hacker);

        System.out.println("序列化完成...");
    }
}

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HackInfo hacker = null;
        //文件流转对象流
        FileInputStream fis = new FileInputStream("hacker.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        hacker = (HackInfo) ois.readObject();
        
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
        System.out.println("Name: "+ hacker.id );
        System.out.println("Team:" + hacker.team);
    }
}

动态加载

参考链接

全文参考自:https://zhuanlan.zhihu.com/p/567962697
动调和深入理解参考自:https://space.bilibili.com/2142877265/

简介

Java的类加载,大概就是把编译好的*.class文件给加载到内存的过程。
image.png
我们主要关注其中,会引起代码执行的部分:

  • 类初始化时,执行静态代码块和静态方法
  • 类使用(实例化)时,执行构造代码块和构造方法

类加载不同阶段执行的代码

Student类里写好静态代码块、静态方法、构造代码块和构造方法

public class Student {
    private String name;
    static int id;

    static {
        System.out.println("静态代码块");
    }

    public static void staticMethod() {
        System.out.println("静态方法");
    }

    {
        System.out.println("构造代码块");
    }

    public Student() {
        System.out.println("无参构造函数");
    }

    public Student(String name) {
        this.name = name;
        System.out.println("有参构造函数");
    }
}

类的加载与初始化

image.png
image.png
从上面可以看出执行类的初始化操作,只会调用静态代码块。
image.png
这里仅仅是进行了类的加载,并不是初始化,所有什么都没调用。
image.png
这里用Class.forName()加载类,会调用静态代码块,跟进看一下
image.png
发现多个重写的方法forName(),这里第二个参数可选是否初始化,设置成false
image.png
未执行静态代码块,说明只执行了类的加载操作。

类的实例化

image.png
显然实例化需要先初始化,所以要执行静态代码块,然后执行自己的构造代码块。

小总结

  • 类的加载,执不执行代码,看实不实现初始化
  • 类的初始化,执行静态代码块
  • 类的实例化,执行构造代码块

类加载器

分类

一般来说分成bootstrap和非bootstrap两类加载器,其中bootstrap ClassLoader是比较底层的,还存在加载类的白名单。
非bootstrap主要又可以分成Extension ClassLoader、Application ClassLoader、User ClassLoader三类。

  • Bootstrap ClassLoader:加载<JAVA_HOME>/lib下的类
  • Extension ClassLoader:加载<JAVA_HOME>/lib/ext下的类
  • Application ClassLoader:加载程序员自定义的类classpath/
  • User ClassLoader:加载任意来源的类

双亲委派

当一个类加载器要加载类时,会让它的逻辑父亲去加载,层层上传,只有当父亲对应的目录下,找不到这个类对应的字节码文件,才让儿子去加载。
每个类加载器都有属于自己的命名空间,不同的类加载器加载同一个类,却会生成不同的对象,使用双亲委派逻辑就不会出现这个问题,一个类名只会被一个加载器加载
image.png

ClassLoader的运行过程

继承关系如下:
image.png
调试一下加载类加载器加载类的代码:
image.png
首先,因为AppClassLoader里没有一个参数的loadClass(),会走到它的父类抽象类ClassLoader里,然后调用俩参数的loadClass()
image.png
接着走进AppClassLoader的loadClass()
image.png
然后是双亲委派的逻辑,调用父亲的loadClass()重复查询
image.png
在父亲这找到了,层层返回class对象
image.png
image.png
image.png
image.png
函数调用顺序:
loadClass() -> findClass() -> defineClass()

任意类加载

创建一个Calc类用来弹计算器,编译成Calc.class

public class Calc {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

URLClassLoader.loadClass()

public static void urlClassLoaderLoadClass() throws Exception{
    //获取要加载的类的路径
//        URL fileURL = new URL("file:///D:\\Codes\\Java\\testtest\\");
    URL httpURL = new URL("http://localhost:8889/");
//        URL jarURL = new URL("jar:http://localhost:8889/Calc.jar!/");
    //创建一个类加载器
    URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{httpURL});
    //加载
    Class clazz = urlClassLoader.loadClass("Calc");
    //实例化
    clazz.newInstance();
}

ClassLoader.defineClass()

public static void classLoaderDefineClass() throws Exception{
    //获取ClassLoader的类对象
    Class loaderClass = ClassLoader.class;
    //获取defineClass()函数对象
    Method defineClassMethod = loaderClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    //设置访问权限,确保可以反射
    defineClassMethod.setAccessible(true);
    //读出Calc.class的所有字节流
    byte[] bytes = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\loaderDemo\\target\\test-classes\\Calc.class"));
    //通过ClassLoader对象获取systemClassLoader对象
    ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
    //调用systemClassLoad的defineClass()方法,这个方法能加载指定类对象
    Class clazz = (Class) defineClassMethod.invoke(sysClassLoader,"Calc",bytes,0,bytes.length);
    //实例化指定的类对象
    clazz.newInstance();
}

这种方法的优点是不用出网,直接把class文件塞过去一股脑读出来就好。
问题是defineClass()是protected方法,在反序列化的场景不能直接反射调用。

Unsafe.defineClass()

这玩意有安全检测,不能直接实例化得到对象,是单例模式(类似Runtime)
好在他类里有现成的属性,存了unsafe对象,叫theUnsafe可以直接拿,
但是theUnsafe又是private的,所以要设置访问权限

public static void unsafeDefineClass() throws Exception{
    //获取Unsafe类
    Class unsafeClass = Unsafe.class;
    //获取theUnsafe属性
    Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
    //设置theUnsafe的访问权限
    theUnsafe.setAccessible(true);
    //强转
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    //读出Calc.class的所有字节流
    byte[] bytes = Files.readAllBytes(Paths.get("D:\\\\Codes\\\\Java\\\\testtest\\Calc.class"));
    //通过ClassLoader对象获取systemClassLoader对象
    ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
    //加载Calc类
    Class clazz = unsafe.defineClass("Calc",bytes,0,bytes.length,sysClassLoader,null);
    //实例化
    clazz.newInstance();
}

小总结

  • URLClassLoader.loadClass(),可以用不同协议实现任意类加载
  • ClassLoader.defineClass(),通过字节码文件来加载类,方法protected
  • Unsafe.defineClass(),同样用字节码文件来加载类,但类不能直接实例化

动态代理

参考链接

https://darkless.cn/2021/10/28/java-sec-proxy/
https://xz.aliyun.com/t/9197
https://www.bilibili.com/video/BV16h411z7o9/?p=3

概念

类似AOP切面编程,想要强化某个方法,但是又不想改变这个方法;或者是提取重复功能代码,降低复杂度。
image.png

静态代理

  • 委托类和代理类都实现同一个接口
  • 代理类里传入委托类对象,重写接口的的方法,在里面加要加的逻辑,并调用委托类对象.接口方法()
package Static;

public interface Rent {
    public void sale();
}

package Static;

public class RentImpl implements Rent {

    @Override
    public void sale() {
        System.out.println("Sale the house....");
    }
}
package Static;

public class RentProxy implements Rent {
    private Rent target;
    public RentProxy(Rent target){
        this.target = target;
    }
    @Override
    public void sale() {
        System.out.println("Do something before sale....");
        target.sale();
    }
}
package Static;

public class Test {
    public static void main(String[] args) {
        RentImpl rentImpl = new RentImpl();
        RentProxy rentProxy = new RentProxy(rentImpl);
        rentProxy.sale();
    }
}

image.png

动态代理

静态代理不够灵活,于是有了动态代理。

package Dynamic;

public interface Rent {
    public void sale();
}
package Dynamic;

public class RentImpl implements Rent {

    @Override
    public void sale() {
        System.out.println("Sale the house....");
    }
}

下面这个类实现了InvocationHandler接口,增强函数的逻辑就在这个类的invoke()方法里面。
invoke()的特性是:后面创建的代理对象rentProxy调用每个方法都会走进这个invoke()

package Dynamic;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class RentInvocationHandler implements InvocationHandler {
    private Object target;
    public RentInvocationHandler(Object target){
        this.target = target;
    }
    //proxy: 要代理的对象
    //method: 要强化的方法
    //args:   要强化方法的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().equals("sale")){
            System.out.println("do something before sale...");
            Object o =  method.invoke(target,args);
            System.out.println("do something after sale....");
            return o;
        }else {
            return method.invoke(target,args);
        }
    }
}

这里创建rentProxy代理对象的语句基本是固定写法,不用怎么研究。

package Dynamic;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
        Rent r = new RentImpl();
        RentInvocationHandler rentInvocationHandler = new RentInvocationHandler(r);
        Rent rentProxy = (Rent) Proxy.newProxyInstance(r.getClass().getClassLoader(), r.getClass().getInterfaces(),rentInvocationHandler);
        rentProxy.sale();
    }
}

安全相关

理想状况下,我们希望能找到这样的gadgets链:
image.png
然而实际情况可能是这样的:
image.png
磁石啊,很多人可能以为女神拒绝了自己,实则不然;假设a是个动态代理对象,而a对应的handler实现类里所重写的invoke()方法里又有危险函数,那么就又可以利用了
image.png

posted @ 2023-08-08 23:13  Jasper_sec  阅读(78)  评论(0编辑  收藏  举报