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


__EOF__

本文作者Jasper
本文链接https://www.cnblogs.com/jasper-sec/p/17615667.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Jasper_sec  阅读(92)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示