一、目录结构
1.1 BOOT-INF和META-INF
在Java源代码文件夹中,BOOT-INF和META-INF通常与Java应用程序的打包和部署有关,尤其是在使用Spring Boot或者Java EE(现在称为Jakarta EE)技术栈时。
META-INF: 这是一个标准的目录名称,用于存放元数据和相关配置文件。在Java中,META-INF目录通常包含以下内容:
- MANIFEST.MF:这是一个清单文件,描述了JAR文件的版本、构建信息、主类以及其他属性。
在Java代码审计中, MANIFEST.MF 文件作为JAR包的核心元数据文件,可能包含影响应用安全的关键配置。以下是需要重点关注的字段及其潜在风险: Main-Class 作用 :指定JAR包的入口类,通过 java -jar 可直接运行。 审计要点 :需验证入口类是否合法,防止攻击者篡改以注入恶意代码。若入口类未经验证或存在逻辑缺陷(如命令执行),可能导致RCE漏洞 。
- pom.properties:在Maven构建的JAR文件中,这个文件包含了项目的信息,如版本号和SCM信息。
版本标识:明确当前构建的 groupId 、 artifactId 和 version ,用于跟踪依赖关系和版本兼容性。 groupId = <项目组ID> # 例如:com.example artifactId = <项目名称> # 例如:demo-app version = <构建版本> # 例如:1.0.0-SNAPSHOT
- services/:在这个目录下,可以放置服务提供者配置文件,用于Java的ServiceLoader机制。
- spring-configuration-metadata.json
spring-configuration-metadata.json 是 Spring Boot 应用中的配置文件元数据文件,用于为 IDE 和开发者提供配置属性的类型、默认值、描述等信息。
BOOT-INF: 这个目录主要与Spring Boot有关,用于存放Spring Boot应用程序的类文件和资源。在Spring Boot的可执行JAR布局中,BOOT-INF通常包含以下内容:
- classes/:这个目录包含了应用程序编译后的类文件。
- lib/:在这个目录中,存放了应用程序依赖的JAR文件。
以下是对这两个目录的具体含义的说明:
- META-INF目录主要用于应用程序的元数据和配置。
- BOOT-INF目录是Spring Boot特有的,用于存放应用程序的实际内容和依赖。
- 需要注意的是,以上内容是基于标准的Java和Spring Boot应用程序的打包结构。在不同的应用服务器或者框架中,这些目录可能会有不同的用途或者不存在。
1.2 springboot项目代码结构(BOOT-INF/classes/com.sxk.stu/…)
二、常见注解
@Controller 注解:标注该类为controller类,可以处理http请求。
@Controller一般要配合模版来使用。现在项目大多是前后端分离,后端处理请求,然后返回JSON格式数
据即可,这样也就不需要模板了。
@ResponseBody 注解:将该注解写在类的外面,表示这个类所有方法的返回的数据直接给浏览器。
@RestController 相当于 @ResponseBody 加上 @Controller
@RequestMapping 注解:配置URL映射 ,可以作用于某个Controller类上,也可以作用于某Controller类下的具体方法中,说白了就是URL中请求路径会直接映射到具体方法中执行代码逻辑。
@RequestParam 注解:将请求参数绑定到你控制器的方法参数上(是springmvc中接
收普通参数的注解),常用于POST请求处理表单。
@PathVariable 注解:接受请求URL路径中占位符的值
2.1 @RestController
@RestController 是 @Controller 和 @ResponseBody 的组合注解 ,用于标记一个类为 RESTful 控制器 ,其主要功能包括:
接收 HTTP 请求 :处理客户端发送的 GET、POST、PUT、DELETE 等请求。
直接返回数据(非视图) :将方法返回值序列化为 JSON/XML 等格式,直接写入 HTTP 响应体(Response Body)。
2.2 @RequestMapping
@RequestMapping 是 Spring MVC 中最核心的注解之一, 用于将 HTTP 请求映射到具体的控制器(Controller)或处理方法(Handler Method) 。通过定义请求路径、HTTP 方法、请求参数等条件,实现请求的精确路由。
2.3 @Component
@Component 是 Spring 框架中的一个核心注解, 用于标记一个类为 Spring 容器管理的组件(Bean) 。通过该注解,Spring 在启动时会自动扫描并实例化这些类,将它们纳入依赖注入(DI)体系。
2.4 @Value
@Value 是 Spring 框架中的一个注解, 用于动态注入值到类的字段、方法参数或构造函数中 。它的核心作用是让代码与外部配置(如配置文件、环境变量、系统属性等)解耦,使应用更灵活、更易维护。
三、Java基础和特性
3.1 动态代理
Java动态代理是一种在运行时动态创建代理对象的技术,用于在不修改原始类代码的情况下,增强其方法的功能(如添加日志、事务管理等)。下面通过一个简单比喻和示例来解释:
3.1.1 比喻理解
假设你开了一家奶茶店( 真实对象 ),但想在外卖平台上接单。由于平台需要管理订单,你决定找一个代理商( 代理对象 )来处理接单前后的工作,比如记录订单、统计销量等,而你只需专注做奶茶。
动态代理就像外卖平台 :你不用自己新建分店( 无需预先编写代理类 ),平台动态为你生成一个代理接口。当顾客下单时,平台先记录订单( 前置处理 ),再通知你制作奶茶,最后统计销量( 后置处理 )。
3.1.2 核心概念
InvocationHandler 接口 :定义代理的逻辑(如记录日志)。
Proxy 类 :动态生成代理对象。
3.1.3 代码示例
- 定义接口和实现类
// 接口:奶茶店
interface MilkTeaShop {
void makeMilktea();
}
// 真实对象:你的奶茶店
class RealShop implements MilkTeaShop {
@Override
public void makeMilktea() {
System.out.println("制作奶茶");
}
}
- 实现InvocationHandler(代理逻辑)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
class ShopProxy implements InvocationHandler {
private Object target; // 真实对象(你的奶茶店)
public ShopProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("记录订单"); // 前置增强
Object result = method.invoke(target, args); // 调用真实对象的方法
System.out.println("统计销量"); // 后置增强
return result;
}
}
- 生成动态代理对象
import java.lang.reflect.Proxy;
public class Demo {
public static void main(String[] args) {
// 真实对象
MilkTeaShop realShop = new RealShop();
// 创建代理处理器,关联真实对象
InvocationHandler handler = new ShopProxy(realShop);
// 动态生成代理对象
MilkTeaShop proxyShop = (MilkTeaShop) Proxy.newProxyInstance(
realShop.getClass().getClassLoader(),
realShop.getClass().getInterfaces(),
handler
);
// 通过代理对象调用方法
proxyShop.makeMilktea();
}
}
- 输出结果
记录订单
制作奶茶
统计销量
3.2 Javassist动态编程
Javassist(Java Programming Assistant)是一个用于 动态编辑Java字节码的类库 ,它允许开发者在运行时直接修改或生成新的类文件,无需深入理解复杂的字节码指令。通过提供 基于源代码的API (如用字符串拼接Java代码),Javassist大幅简化了字节码操作,开发者可以像写普通Java代码一样动态创建类、添加方法或修改方法逻辑,常用于实现AOP(面向切面编程)、动态代理、性能监控工具等场景,相比反射或动态代理更灵活,适合需要深度定制类行为的场景(例如热修复、代码增强)。
动态编程是 相对于静态编程而言的一种编程形式,对于静态编程而言,类型检查是在编译时完 成的,但是对于动态编程来说,类型检查是在运行时完成的。因此所谓动态编程就 是绕过编译过程在运行时进行操作的技术
一般来说,在依赖关系需要动态确认或者需要在运行时动态插入代码 的环境中,需要使用动态编程
Java 字节码以二进制形式存储在 class 文件中,每一个 class 文件都包含一个 Java 类或接口。Javassist 就是一个用来处理 Java 字节码的类库,其主要优点在于简 单、便捷。用户不需要了解虚拟机指令,就可以直接使用 Java 编码的形式,并且可 以动态改变类的结构,或者动态生成类
Javassist 中最为重要的是 ClassPool、CtClass 、CtMethod 以及 CtField 这 4 个类。
● ClassPool:一个基于 HashMap 实现的 CtClass 对象容器,其中键是类名称, 值是表示该类的 CtClass 对象。默认的 ClassPool 使用与底层 JVM 相同的类路 径,因此在某些情况下,可能需要向 ClassPool 添加类路径或类字节。
● CtClass:表示一个类,这些 CtClass 对象可以从 ClassPool 获得。
● CtMethods:表示类中的方法。
● CtFields:表示类中的字段。
Javassit官方文档中给出的代码示例如下。
- 示例
package com.ms08067;
import javassist.*;
public class TestJavassist {
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cls = pool.makeClass("Test");
CtField param = new CtField(pool.get("java.lang.String"), "test", cls);
param.setModifiers(Modifier.PRIVATE);
cls.addField(param, CtField.Initializer.constant("whoami"));
CtConstructor cons = new CtConstructor(new CtClass[]{}, cls);
cons.setBody("{test = \"whoami\";}");
cls.addConstructor(cons);
cls.writeFile("./");
}
//
// public class Test{
// private String test = "test";
// public Test(){
// this.test = "whoami";
// }
// }
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.3 类加载过程
Java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类装载器的作用其实就是类的加载。
当运行某个类的时候,如果发现这个类还没有被加载,那么就会如上过程进行加载。
3.3.1 类加载器的种类
Bootstrap ClassLoader(启动类加载器)这个类加载器负责将一些核心的,被JVM识别的类加载进来,用C++实现,与JVM是一体的。
Extension classLoader(扩展类加载器)这个类加载器用来加载Java的扩展库
Applicaiton ClassLoader(App类加载器/系统类加载器)用于加载我们自己定义编写的类
User ClassLoader (用户自己实现的加载器)当实际需要自己掌控类加载过程时才会用到。
3.3.2 相关方法
getParent()、loadClass()、findClass()、findLoadedClass()、defineClass()、resolveClass()
3.3.3 双亲委派机制
- 定义:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这个机制就叫双亲委派机制。
- 双亲委派机制的实现:
- 首先,检查请求的类是否已经被加载过了
- 未加载,则请求父类加载器去加载对应路径下的类,
- 如果加载不到,才由下面的子类依次去加载。
Java. lang.string之本地加载器之扩展加载器子根加载器。
3.3.4 自定义类加载器
package com.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class DemoClassLoader extends ClassLoader {
private byte[] bytes ;
private String name = "";
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, InvocationTargetException {
String clzzName = "com.test.Hello";
byte[] testBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 52, 0, 28, 10, 0, 6, 0, 14, 9, 0, 15, 0, 16, 8, 0, 17, 10, 0, 18, 0, 19, 7, 0, 20, 7, 0, 21, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 115, 97, 121, 72, 101, 108, 108, 111, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 10, 72, 101, 108, 108, 111, 46, 106, 97, 118, 97, 12, 0, 7, 0, 8, 7, 0, 22, 12, 0, 23, 0, 24, 1, 0, 6, 104, 101, 108, 108, 111, 126, 7, 0, 25, 12, 0, 26, 0, 27, 1, 0, 14, 99, 111, 109, 47, 116, 101, 115, 116, 47, 72, 101, 108, 108, 111, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0, 2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 11, 0, 8, 0, 1, 0, 9, 0, 0, 0, 37, 0, 2, 0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 5, 0, 8, 0, 6, 0, 1, 0, 12, 0, 0, 0, 2, 0, 13
};
DemoClassLoader demo = new DemoClassLoader(clzzName,testBytes);
Class clazz = demo.loadClass(clzzName);
Constructor constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(obj);
}
public DemoClassLoader(String name, byte[] bytes){
this.name = name;
this.bytes = bytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if(name.equals(this.name)) {
return defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}
}
四、注入漏洞
4.1 SQL注入
SQL注入的根本原因在于SQL语句的拼接构造。
4.1.1 JDBC字符串拼接
JDBC有两种方式执行SQL语句,分别为PreparedStatement和Statement。
- Statement方法在每次执行时都需要编译
- PreparedStatement会对SQL语句进行预编译,后续无需重新编译
理论上PrepareStatement的效率和安全性会比Statement要好。正确地使用 PrepareStatement 可以有效避免 SQL 注入的产生(但是有的地方不能使用预编译),使用“?”作为占位符时,填入对应字段的值会进行严格的类型检查
4.1.2 框架使用不当造成SQL注入
如今的Java项目或多或少会使用对JDBC进行更抽象封装的持久化框架,如MyBatis 和Hibernate。
1)mybatis
#
的$
的区别#
号会对语句进行预编译${ } 只是进行string 替换,动态解析SQL的时候会进行变量替换
使用#{Parameter}构造SQL:
<select id="getUsername" resultType="com.demo.bean.User">
select id,name,age from user where name = #{name}
</select>
执行结果:
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3a8ed7c6]
==> Preparing: select * from user where name = ?
==> Parameters: Yu(String).
<== Columns: Id, name
<== Row: 1, Yu
<== Total: 1
- 使用${Parameter}构造SQL
<select id="getUsername" resultType="com.demo.bean.User">
select id,name,age from user where name = ${name}
</select>
执行结果:
==> Preparing: select * from user where name = 'Yu'
==> Parameters:
<== Columns: Id, name
<== Row: 1, Yu
<== Total: 1
========================================================================
==> Preparing: select * from user where name = 'Yu' or 1=1 limit 0,1
==> Parameters:
<== Columns: Id, name
<== Row: 1, Yu
<== Total: 1
2)Hibernate
HQL的几种正确用法
- 位置参数 (Positional parameter):
String parameter = "z1ng";
Query<User> query = session.createQuery("from com.demo.bean.User where name = ?1", User.class);
query.setParameter(1, parameter);
执行结果
Hibernate:
select
user0_.id as id1_0_,
user0_.name as name2_0_
from
User user0_
where
user0_.name=?
- 命名参数 (named parameter)
Query<User> query = session.createQuery("from com.demo.bean.User where name = ?1", User.class);
String parameter = "z1ng";
Query<User> query = session.createQuery("from com.demo.bean.User where name = :name", User.class);
query.setParameter("name", parameter);
- 命名参数列表(named parameter list)
List<String> names = Arrays.asList("z1ng", "z2ng");
Query<User> query = session.createQuery("from com.demo.bean.User where name in (:names)", User.class);
query.setParameter("names", names);
- 类实例 (JavaBean)
user1.setName("z1ng");
Query<User> query = session.createQuery("from com.demo.bean.User where name =:name", User.class);
query.setProperties(user1);
Native SQL注入
Hibernate支持原生的SQL语句执行,与JDBC的SQL注入相同,直接拼接构造SQL语句会导致安全隐患的产生,应采用参数绑定的方式构造SQL语句。
- 拼接构造:
Query<User> query = session.createNativeQuery("select * from user where name = '"+parameter+"'");
- 参数绑定:
Query<User> query = session.createNativeQuery("select * from user where name = :name");
query.setParameter("name",parameter);
4.1.3 预编译一些场景下的局限
下面两个地方不能使用预编译(预编译会加单引号,下面的地方不能使用单引号):
- 表名作为变量时,需使用拼接
select * from `user`
select * from 'user' #报错
- order by后需要使用拼接
select * from user order by name
select * from user order by 'name' #语义不对
4.2 命令注入
某种开发需求中,需要引入对系统本地命令的支持来完成某些特定的功能。但未对输入进行充分的过滤,导致漏洞产生。
- Java中Runtime类可以调用系统命令:
String cmd = req.getParameter("cmd");
Process process = Runtime.getRuntime().exec(cmd);
- Runtime类的底层是调用 ProcessBuilder类,所以它也可以执行系统命令。
ProcessBuilder pb = new ProcessBuilder(cmd);
Process process = pb.start();
思考:如下代码是否存在命令执行呢?
String cmd = "ping "+url;
Process process = Runtime.getRuntime().exec(cmd);
//下面代码是返回输出的
InputStream in = process.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int i = -1;
while ((i = in.read(b)) != -1) {
byteArrayOutputStream.write(b, 0, i);
}
不存在,在该 Java 程序的处理中,www.baidu.com&ipconfig
被当作一个完整的字符串而非两条命令。
如下代码是存在命令执行的:
String[] cmdarr = { "cmd", "/c","ping "+cmd};
Process process = Runtime.getRuntime().exec(cmdarr);
InputStream in = process.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int i = -1;
while ((i = in.read(b)) != -1) {
byteArrayOutputStream.write(b, 0, i);
}
return new String(byteArrayOutputStream.toByteArray());
也就是说,字符串拼接的方式不存在命令注入,数组的形式存在。
4.3 代码注入
代码注入(Code Injection)与命令注入相似,指在正常的 Java 程序中注入一段 Java 代码并执行。相比于命令注入,代码注入更具有灵活性,注入的代码可以写入或修改系统文件,甚至可以直接注入执行系统命令的代码。在实际的漏洞利用中,直接进行系统命令执行常常受到各方面的因素限制,而代码注入因为灵活多变,可利用 Java 的各种技术突破限制,造成更大的危害。
程序中存在某种功能可以直接执行java代码,比如:反射机制。
以下示例展示了一个根据用户输入的类名进行动态实例化和调用方法的一个过程。
String ClassName = req.getParameter("ClassName");
String MethodName = req.getParameter("Method");
String[] Args = new String[]{req.getParameter("Args").toString()};
try {
Class clazz = Class.forName(ClassName);
Constructor constructor = clazz.getConstructor(String[].class);
Object obj = constructor.newInstance(new Object[]{Args});
Method method = clazz.getMethod(MethodName);
method.invoke(obj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
等效于链式调用:
ProcessBuilder("calc").start()
Apache Commons collections 反序列化漏洞其中就存在了这个问题:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
4.4 表达式注入
表达式注入这一概念最早出现在2012年12月的一篇论文《Remote Code Execution withEL Injection Vulnerabilities》中,文中详细阐述了表达式注入的成因以及危害。表达式注入在互联网上造成过巨大的危害,例如Struts2系列曾几次因OGNL表达式引起远程代码执行。从本质上表达式只是Java代码的另一种形式。
什么是Java表达式注入?
想象你有一个智能机器人(Java应用),它能根据你输入的指令(表达式)完成任务。比如,输入 1+1 ,它会回答 2 ;输入 “hello”.toUpperCase() ,它会输出 HELLO 。这些指令通常由开发者预设,但 如果机器人错误地接受了用户提供的恶意指令 ,就会引发漏洞。 例如,用户输入 System.exit(1) ,机器人可能会直接关闭自己,这就是表达式注入攻击。
漏洞是如何发生的?
动态表达式解析 :某些Java框架(如Spring的SpEL、Struts2的OGNL)允许动态解析表达式,用于灵活处理数据。
未过滤的用户输入 :如果应用直接将用户输入(如URL参数、表单数据)拼接到表达式中,攻击者可构造恶意表达式。
执行危险操作 :恶意表达式可能执行系统命令、删除文件、窃取数据等。
4.4.1 JSP中的表达式
<%@ page contentType="text/html;charset=UTF-8" language="Java" %>
<html>
<head>
<title>EL表达式实例页面</title>
</head>
<body>
<center>
<h3> 输入的name值为:${param.name}</h3>
</center>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="Java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
${Runtime.getRuntime().exec("calc")}
</body>
</html>
4.4.2 SpEL
在SpEL中,EvaluationContext是用于评估表达式和解析属性、方法以及字段并帮助执行类型转换的接口,该接口有两种实现,分别为SimpleEvaluationContext和StandardEvaluationContext,在默认情况下使用StandardEvaluationContext对表达式进行评估。
- SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
- StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。用户可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
当使用 StandardEvaluationContext 进行上下文评估时,由于 StandardEvaluation Context 权限过大,可以执行 Java 任意代码
在使用SimpleEvaluationContext 进行上下文评估时,无法使用 Runtime.class 执行任何系统命令。
- 例子
假设一个网站使用Spring的SpEL表达式查询用户信息:
// 用户输入userId,拼接到表达式中
String expression = "userRepository.findById(" + userId + ")";
SpelExpressionParser parser = new SpelExpressionParser();
User user = parser.parseExpression(expression).getValue();
正常情况下,用户输入 123 ,表达式变成 userRepository.findById(123) 。 但攻击者输入 T(java.lang.Runtime).getRuntime().exec('rm -rf /')
,服务器可能执行删除文件的命令!
真实案例 Struts2漏洞 :
- 过去多次因OGNL表达式注入导致远程代码执行(如S2-045)。
- Spring Boot漏洞 :错误处理中泄露SpEL表达式,引发注入攻击(如CVE-2022-22963)。
4.5 模版注入
WEB应用程序中广泛使用模板引擎来进行页面的定制化呈现,用户可以通过模板定制化展示符合自身特征的页面。模板引擎支持页面定制展示的同时也带来了一定安全风险。
Java模板注入漏洞 就像让陌生人给你的「智能文档生成器」下指令,如果没做好检查,对方可能偷偷修改文档生成规则,导致生成恶意内容或执行危险操作。以下是通俗解释:
- 什么是Java模板注入?
想象你有一个 文档生成器 (模板引擎),能根据预设的模板生成网页、邮件等。比如模板里写:
<p>你好,${username}!今天是${date}。</p>
当用户输入 username=张三 ,生成:
<p> 你好,张三!今天是2023-10-01。 </p >
如果用户输入的 username 不是普通文本,而是模板语法 ,比如 ${7 * 7} ,生成结果可能变成:
<p>你好,49!今天是2023-10-01。</p>
如果攻击者注入更危险的代码(如执行系统命令),就会引发漏洞。
- 漏洞是如何发生的?
模板引擎支持动态语法 :Java的模板引擎(如Thymeleaf、FreeMarker、Velocity)允许在模板中嵌入逻辑(如计算、循环、调用函数)。
用户输入直接拼进模板 :如果用户输入的内容未经处理,直接被当作模板的一部分解析,攻击者可注入恶意代码。
服务器执行恶意逻辑 :模板引擎解析时,可能执行攻击者的代码(如删除文件、窃取数据)。
- 举例
假设一个网站用模板生成欢迎语:
// 用户输入name,直接拼到模板里 String template = "<p>欢迎," + name + "!</p>"; ThymeleafEngine.render(template);
正常输入 : name=小明 → 生成
欢迎,小明!
。恶意输入 : name=${T(java.lang.Runtime).getRuntime().exec(‘rm -rf /‘)} → 模板引擎解析时,会执行删除文件的命令!
4.5.1 Freemarker模板注入
1)内建函数
FreeMarker中预制了大量的内建函数,极大地增强和拓展了模板的语言功能。在增强功能的同时,也可能引发一些危险操作,若研发人员不加以限制,则很可能产生安全隐患。
2)New函数的利用
new函数可以创建一个继承自freemarker.template.TemplateModel 类的实例,查阅源码会发现freemarker.template.utility.Execute#exec可以执行任意代码,因此可以通过new函数实例化一个Execute对象并执行exec方法造成任意代码被执行,如图所示。
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
通过阅读源码发现freemarker.template.utility包中以下几个类都可以被利用来执行恶意代码:
3)Api函数的利用
Api函数可以用来访问Java API,使用方法为value?api.someJavaMethod(),相当于value.someJavaMethod()。因此可以利用Api方法通过getClassLoader来获取一个类加载器,进而加载恶意类。
<#assign classLoader=object?api.class.getClassLoader()>
${classLoader.loadClass("Evil.class")}
4.6 JNDI注入
4.7 XML注入
五、敏感信息泄露漏洞
六、XSS
七、XXE
XML解析一般在导入配置、数据传输接口等场景可能会用到。涉及到XML文件处理的场景应留意下XML解析器是否禁用外部实体,从而判断是否存在XXE。
关注:
javax.xml.parsers.DocumentBuilder
javax.xml.parsers.SAXParser
javax.xml.parsers.SAXParserFactory
javax.xml.transform.TransformerFactory
javax.xml.validation.Validator
javax.xml.validation.SchemaFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.transform.sax.SAXSource
org.xml.sax.XMLReader
org.xml.sax.helpers.XMLReaderFactory
org.dom4j.io.SAXReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.bind.Unmarshaller
javax.xml.xpath.XpathExpression
javax.xml.stream.XMLStreamReader
org.apache.commons.digester3.Digester
xlsx-streamer poi-ooxml
Documentbuilder|DocumentBuilderFactory|SAXReader|SAXParser|SAXParserFactory|SAXBuilder|TransformerFactory|reqXml|getInputStream|XMLReaderFactory|.newInstance|SchemaFactory|SAXTransformerFactory|javax.xml.bind|XMLRear
使用 XML 解析器时需要设置其属性,禁止使用外部实体。XML 解析器的安全 使用可参考。
八、文件上传
zipslip
pdf文件上传的xss,跟浏览器相关。
九、SSRF
十、REDOS
十一、组件漏洞
Hutool<=5.8.12工具类的xmlutil.readObjectFromXml
Fastjson < 1.2.83
Xstream 官网列出了漏洞和poc,函数fromXml
Log4j
hessian <=4.0.06
common collections
十二、框架漏洞
- Spring
- Struts2
- 通用
审计思路
工具
IRFY
参考
- https://blog.csdn.net/qq_45158261/article/details/136258713?ops_request_misc=&request_id=&biz_id=102&utm_term=java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-136258713.142^v102^pc_search_result_base3&spm=1018.2226.3001.4187
- https://www.cnblogs.com/yokan/p/16102570.html 【《java代码审计》学习笔记】