语雀不充钱出不了网,纯纯跳板,不定时更新。
反射
概念
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文件给加载到内存的过程。
我们主要关注其中,会引起代码执行的部分:
- 类初始化时,执行静态代码块和静态方法
- 类使用(实例化)时,执行构造代码块和构造方法
类加载不同阶段执行的代码
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("有参构造函数");
}
}
类的加载与初始化
从上面可以看出执行类的初始化操作,只会调用静态代码块。
这里仅仅是进行了类的加载,并不是初始化,所有什么都没调用。
这里用Class.forName()加载类,会调用静态代码块,跟进看一下
发现多个重写的方法forName(),这里第二个参数可选是否初始化,设置成false
未执行静态代码块,说明只执行了类的加载操作。
类的实例化
显然实例化需要先初始化,所以要执行静态代码块,然后执行自己的构造代码块。
小总结
- 类的加载,执不执行代码,看实不实现初始化
- 类的初始化,执行静态代码块
- 类的实例化,执行构造代码块
类加载器
分类
一般来说分成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:加载任意来源的类
双亲委派
当一个类加载器要加载类时,会让它的逻辑父亲去加载,层层上传,只有当父亲对应的目录下,找不到这个类对应的字节码文件,才让儿子去加载。
每个类加载器都有属于自己的命名空间,不同的类加载器加载同一个类,却会生成不同的对象,使用双亲委派逻辑就不会出现这个问题,一个类名只会被一个加载器加载
ClassLoader的运行过程
继承关系如下:
调试一下加载类加载器加载类的代码:
首先,因为AppClassLoader里没有一个参数的loadClass(),会走到它的父类抽象类ClassLoader里,然后调用俩参数的loadClass()
接着走进AppClassLoader的loadClass()
然后是双亲委派的逻辑,调用父亲的loadClass()重复查询
在父亲这找到了,层层返回class对象
函数调用顺序:
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切面编程,想要强化某个方法,但是又不想改变这个方法;或者是提取重复功能代码,降低复杂度。
静态代理
- 委托类和代理类都实现同一个接口
- 代理类里传入委托类对象,重写接口的的方法,在里面加要加的逻辑,并调用委托类对象.接口方法()
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();
}
}
动态代理
静态代理不够灵活,于是有了动态代理。
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链:
然而实际情况可能是这样的:
磁石啊,很多人可能以为女神拒绝了自己,实则不然;假设a是个动态代理对象,而a对应的handler实现类里所重写的invoke()方法里又有危险函数,那么就又可以利用了